[
  {
    "path": ".crowdin/location_names.csv",
    "content": "key,zh-Hant,en,ja,ko,ru,vi,zh-Hans\r\n連江,連江,Lienchiang,,,,,\r\n宜蘭,宜蘭,Yilan,,,,,\r\n彰化,彰化,Changhua,,,,,\r\n南投,南投,Nantou,,,,,\r\n雲林,雲林,Yunlin,,,,,\r\n屏東,屏東,Pingtung,,,,,\r\n基隆,基隆,Keelung,,,,,\r\n臺北,臺北,Taipei,,,,,\r\n新北,新北,New Taipei,,,,,\r\n臺南,臺南,Tainan,,,,,\r\n桃園,桃園,Taoyuan,,,,,\r\n嘉義,嘉義,Chiayi,,,,,\r\n金門,金門,Kinmen,,,,,\r\n高雄,高雄,Kaohsiung,,,,,\r\n臺東,臺東,Taitung,,,,,\r\n花蓮,花蓮,Hualien,,,,,\r\n澎湖,澎湖,Penghu,,,,,\r\n新竹,新竹,Hsinchu,,,,,\r\n臺中,臺中,Taichung,,,,,\r\n苗栗,苗栗,Miaoli,,,,,\r\n成功,成功,Chenggong,,,,,\r\n佳冬,佳冬,Jiadong,,,,,\r\n麥寮,麥寮,Mailiao,,,,,\r\n綠島,綠島,Lüdao,,,,,\r\n蘭嶼,蘭嶼,Lanyu,,,,,\r\n田中,田中,Tianzhong,,,,,\r\n社頭,社頭,Shetou,,,,,\r\n竹田,竹田,Zhutian,,,,,\r\n萬丹,萬丹,Wandan,,,,,\r\n三灣,三灣,Sanwan,,,,,\r\n峨眉,峨眉,Emei,,,,,\r\n南庄,南庄,Nanzhuang,,,,,\r\n太保,太保,Taibao,,,,,\r\n中埔,中埔,Zhongpu,,,,,\r\n番路,番路,Fanlu,,,,,\r\n水上,水上,Shuishang,,,,,\r\n員林,員林,Yuanlin,,,,,\r\n小港,小港,Xiaogang,,,,,\r\n蘇澳,蘇澳,Su'ao,,,,,\r\n五結,五結,Wujie,,,,,\r\n壯圍,壯圍,Zhuangwei,,,,,\r\n南竿,南竿,Nangan,,,,,\r\n莒光,莒光,Juguang,,,,,\r\n烏坵,烏坵,Wuqiu,,,,,\r\n羅東,羅東,Luodong,,,,,\r\n員山,員山,Yuanshan,,,,,\r\n冬山,冬山,Dongshan,,,,,\r\n三星,三星,Sanxing,,,,,\r\n大同,大同,Datong,,,,,\r\n竹東,竹東,Zhudong,,,,,\r\n新埔,新埔,Xinpu,,,,,\r\n關西,關西,Guanxi,,,,,\r\n湖口,湖口,Hukou,,,,,\r\n芎林,芎林,Qionglin,,,,,\r\n橫山,橫山,Hengshan,,,,,\r\n北埔,北埔,Beipu,,,,,\r\n五峰,五峰,Wufeng,,,,,\r\n龍井,龍井,Longjing,,,,,\r\n大雅,大雅,Daya,,,,,\r\n沙鹿,沙鹿,Shalu,,,,,\r\n梧棲,梧棲,Wuqi,,,,,\r\n湖西,湖西,Huxi,,,,,\r\n金峰,金峰,Jinfeng,,,,,\r\n太麻里,太麻里,Taimali,,,,,\r\n卓蘭,卓蘭,Zhuolan,,,,,\r\n大湖,大湖,Dahu,,,,,\r\n公館,公館,Gongguan,,,,,\r\n銅鑼,銅鑼,Tongluo,,,,,\r\n頭屋,頭屋,Touwu,,,,,\r\n三義,三義,Sanyi,,,,,\r\n西湖,西湖,Xihu,,,,,\r\n造橋,造橋,Zaoqiao,,,,,\r\n獅潭,獅潭,Shitan,,,,,\r\n和美,和美,Hemei,,,,,\r\n線西,線西,Xianxi,,,,,\r\n伸港,伸港,Shengang,,,,,\r\n秀水,秀水,Xiushui,,,,,\r\n花壇,花壇,Huatan,,,,,\r\n芬園,芬園,Fenyuan,,,,,\r\n溪湖,溪湖,Xihu,,,,,\r\n東石,東石,Dongshi,,,,,\r\n大村,大村,Dacun,,,,,\r\n埔鹽,埔鹽,Puyan,,,,,\r\n埔心,埔心,Puxin,,,,,\r\n永靖,永靖,Yongjing,,,,,\r\n二水,二水,Ershui,,,,,\r\n二林,二林,Erlin,,,,,\r\n埤頭,埤頭,Pitou,,,,,\r\n芳苑,芳苑,Fangyuan,,,,,\r\n大城,大城,Dacheng,,,,,\r\n竹塘,竹塘,Zhutang,,,,,\r\n溪州,溪州,Xizhou,,,,,\r\n埔里,埔里,Puli,,,,,\r\n草屯,草屯,Caotun,,,,,\r\n竹山,竹山,Zhushan,,,,,\r\n集集,集集,Jiji,,,,,\r\n名間,名間,Mingjian,,,,,\r\n鹿谷,鹿谷,Lugu,,,,,\r\n中寮,中寮,Zhongliao,,,,,\r\n魚池,魚池,Yuchi,,,,,\r\n國姓,國姓,Guoxing,,,,,\r\n水里,水里,Shuili,,,,,\r\n信義,信義,Xinyi,,,,,\r\n仁愛,仁愛,Ren'ai,,,,,\r\n斗六,斗六,Douliu,,,,,\r\n斗南,斗南,Dounan,,,,,\r\n虎尾,虎尾,Huwei,,,,,\r\n西螺,西螺,Xiluo,,,,,\r\n土庫,土庫,Tuku,,,,,\r\n北港,北港,Beigang,,,,,\r\n古坑,古坑,Gukeng,,,,,\r\n大埤,大埤,Dapi,,,,,\r\n莿桐,莿桐,Citong,,,,,\r\n林內,林內,Linnei,,,,,\r\n二崙,二崙,Erlun,,,,,\r\n崙背,崙背,Lunbei,,,,,\r\n東勢,東勢,Dongshi,,,,,\r\n褒忠,褒忠,Baozhong,,,,,\r\n元長,元長,Yuanchang,,,,,\r\n水林,水林,Shuilin,,,,,\r\n朴子,朴子,Puzi,,,,,\r\n大林,大林,Dalin,,,,,\r\n民雄,民雄,Minxiong,,,,,\r\n溪口,溪口,Xikou,,,,,\r\n新港,新港,Xingang,,,,,\r\n六腳,六腳,Liujiao,,,,,\r\n義竹,義竹,Yizhu,,,,,\r\n鹿草,鹿草,Lucao,,,,,\r\n竹崎,竹崎,Zhuqi,,,,,\r\n梅山,梅山,Meishan,,,,,\r\n大埔,大埔,Dapu,,,,,\r\n阿里山,阿里山,Alishan,,,,,\r\n潮州,潮州,Chaozhou,,,,,\r\n長治,長治,Changzhi,,,,,\r\n麟洛,麟洛,Linluo,,,,,\r\n九如,九如,Jiuru,,,,,\r\n里港,里港,Ligang,,,,,\r\n鹽埔,鹽埔,Yanpu,,,,,\r\n高樹,高樹,Gaoshu,,,,,\r\n萬巒,萬巒,Wanluan,,,,,\r\n內埔,內埔,Neipu,,,,,\r\n新埤,新埤,Xinpi,,,,,\r\n崁頂,崁頂,Kanding,,,,,\r\n南州,南州,Nanzhou,,,,,\r\n琉球,琉球,Liuqiu,,,,,\r\n三地門,三地門,Sandimen,,,,,\r\n霧臺,霧臺,Wutai,,,,,\r\n瑪家,瑪家,Majia,,,,,\r\n泰武,泰武,Taiwu,,,,,\r\n來義,來義,Laiyi,,,,,\r\n春日,春日,Chunri,,,,,\r\n獅子,獅子,Shizi,,,,,\r\n鹿野,鹿野,Luye,,,,,\r\n池上,池上,Chishang,,,,,\r\n延平,延平,Yanping,,,,,\r\n光復,光復,Guangfu,,,,,\r\n瑞穗,瑞穗,Ruisui,,,,,\r\n富里,富里,Fuli,,,,,\r\n馬公,馬公,Magong,,,,,\r\n白沙,白沙,Baisha,,,,,\r\n西嶼,西嶼,Xiyu,,,,,\r\n望安,望安,Wang'an,,,,,\r\n七美,七美,Qimei,,,,,\r\n暖暖,暖暖,Nuannuan,,,,,\r\n大安,大安,Da'an,,,,,\r\n文山,文山,Wenshan,,,,,\r\n鹽埕,鹽埕,Yancheng,,,,,\r\n新興,新興,Xinxing,,,,,\r\n前金,前金,Qianjin,,,,,\r\n前鎮,前鎮,Qianzhen,,,,,\r\n頭城,頭城,Toucheng,,,,,\r\n南澳,南澳,Nan'ao,,,,,\r\n竹北,竹北,Zhubei,,,,,\r\n新豐,新豐,Xinfeng,,,,,\r\n苑裡,苑裡,Yuanli,,,,,\r\n通霄,通霄,Tongxiao,,,,,\r\n竹南,竹南,Zhunan,,,,,\r\n後龍,後龍,Houlong,,,,,\r\n鹿港,鹿港,Lukang,,,,,\r\n福興,福興,Fuxing,,,,,\r\n臺西,臺西,Taixi,,,,,\r\n四湖,四湖,Sihu,,,,,\r\n口湖,口湖,Kouhu,,,,,\r\n布袋,布袋,Budai,,,,,\r\n東港,東港,Donggang,,,,,\r\n枋寮,枋寮,Fangliao,,,,,\r\n新園,新園,Xinyuan,,,,,\r\n林邊,林邊,Linbian,,,,,\r\n車城,車城,Checheng,,,,,\r\n滿州,滿州,Manzhou,,,,,\r\n枋山,枋山,Fangshan,,,,,\r\n牡丹,牡丹,Mudan,,,,,\r\n卑南,卑南,Beinan,,,,,\r\n東河,東河,Donghe,,,,,\r\n吉安,吉安,Ji'an,,,,,\r\n壽豐,壽豐,Shoufeng,,,,,\r\n秀林,秀林,Xiulin,,,,,\r\n楠梓,楠梓,Nanzi,,,,,\r\n鳳山,鳳山,Fengshan,,,,,\r\n大寮,大寮,Daliao,,,,,\r\n大樹,大樹,Dashu,,,,,\r\n大社,大社,Dashe,,,,,\r\n仁武,仁武,Renwu,,,,,\r\n鳥松,鳥松,Niaosong,,,,,\r\n岡山,岡山,Gangshan,,,,,\r\n橋頭,橋頭,Qiaotou,,,,,\r\n燕巢,燕巢,Yanchao,,,,,\r\n田寮,田寮,Tianliao,,,,,\r\n阿蓮,阿蓮,Alian,,,,,\r\n路竹,路竹,Luzhu,,,,,\r\n湖內,湖內,Hunei,,,,,\r\n旗山,旗山,Qishan,,,,,\r\n美濃,美濃,Meinong,,,,,\r\n六龜,六龜,Liugui,,,,,\r\n甲仙,甲仙,Jiaxian,,,,,\r\n杉林,杉林,Shanlin,,,,,\r\n內門,內門,Neimen,,,,,\r\n茂林,茂林,Maolin,,,,,\r\n桃源,桃源,Taoyuan,,,,,\r\n那瑪夏,那瑪夏,Namaxia,,,,,\r\n永和,永和,Yonghe,,,,,\r\n新店,新店,Xindian,,,,,\r\n土城,土城,Tucheng,,,,,\r\n蘆洲,蘆洲,Luzhou,,,,,\r\n五股,五股,Wugu,,,,,\r\n坪林,坪林,Pinglin,,,,,\r\n平溪,平溪,Pingxi,,,,,\r\n烏來,烏來,Wulai,,,,,\r\n豐原,豐原,Fengyuan,,,,,\r\n后里,后里,Houli,,,,,\r\n神岡,神岡,Shengang,,,,,\r\n新社,新社,Xinshe,,,,,\r\n石岡,石岡,Shigang,,,,,\r\n外埔,外埔,Waipu,,,,,\r\n大肚,大肚,Dadu,,,,,\r\n新營,新營,Xinying,,,,,\r\n鹽水,鹽水,Yanshui,,,,,\r\n白河,白河,Baihe,,,,,\r\n後壁,後壁,Houbi,,,,,\r\n麻豆,麻豆,Madou,,,,,\r\n下營,下營,Xiaying,,,,,\r\n六甲,六甲,Liujia,,,,,\r\n官田,官田,Guantian,,,,,\r\n大內,大內,Danei,,,,,\r\n佳里,佳里,Jiali,,,,,\r\n學甲,學甲,Xuejia,,,,,\r\n西港,西港,Xigang,,,,,\r\n新化,新化,Xinhua,,,,,\r\n新市,新市,Xinshi,,,,,\r\n安定,安定,Anding,,,,,\r\n玉井,玉井,Yujing,,,,,\r\n楠西,楠西,Nanxi,,,,,\r\n南化,南化,Nanhua,,,,,\r\n左鎮,左鎮,Zuozhen,,,,,\r\n仁德,仁德,Rende,,,,,\r\n歸仁,歸仁,Guiren,,,,,\r\n關廟,關廟,Guanmiao,,,,,\r\n龍崎,龍崎,Longqi,,,,,\r\n永康,永康,Yongkang,,,,,\r\n北,北,North,,,,,\r\n林園,林園,Linyuan,,,,,\r\n茄萣,茄萣,Qieding,,,,,\r\n永安,永安,Yong'an,,,,,\r\n彌陀,彌陀,Mituo,,,,,\r\n梓官,梓官,Ziguan,,,,,\r\n淡水,淡水,Tamsui,,,,,\r\n瑞芳,瑞芳,Ruifang,,,,,\r\n林口,林口,Linkou,,,,,\r\n三芝,三芝,Sanzhi,,,,,\r\n八里,八里,Bali,,,,,\r\n大甲,大甲,Dajia,,,,,\r\n北門,北門,Beimen,,,,,\r\n安南,安南,Annan,,,,,\r\n蘆竹,蘆竹,Luzhu,,,,,\r\n龜山,龜山,Guishan,,,,,\r\n復興,復興,Fuxing,,,,,\r\n東,東,East,,,,,\r\n西,西,West,,,,,\r\n達仁,達仁,Daren,,,,,\r\n大武,大武,Dawu,,,,,\r\n關山,關山,Guanshan,,,,,\r\n海端,海端,Haiduan,,,,,\r\n香山,香山,Xiangshan,,,,,\r\n礁溪,礁溪,Jiaoxi,,,,,\r\n玉里,玉里,Yuli,,,,,\r\n卓溪,卓溪,Zhuoxi,,,,,\r\n頭份,頭份,Toufen,,,,,\r\n清水,清水,Qingshui,,,,,\r\n南,南,South,,,,,\r\n安平,安平,Anping,,,,,\r\n中西,中西,West Central,,,,,\r\n大溪,大溪,Daxi,,,,,\r\n八德,八德,Bade,,,,,\r\n大園,大園,Dayuan,,,,,\r\n楊梅,楊梅,Yangmei,,,,,\r\n七堵,七堵,Qidu,,,,,\r\n中正,中正,Zhongzheng,,,,,\r\n中山,中山,Zhongshan,,,,,\r\n安樂,安樂,Anle,,,,,\r\n三峽,三峽,Sanxia,,,,,\r\n鶯歌,鶯歌,Yingge,,,,,\r\n中和,中和,Zhonghe,,,,,\r\n樹林,樹林,Shulin,,,,,\r\n深坑,深坑,Shenkeng,,,,,\r\n板橋,板橋,Banqiao,,,,,\r\n石碇,石碇,Shiding,,,,,\r\n新莊,新莊,Xinzhuang,,,,,\r\n泰山,泰山,Taishan,,,,,\r\n三重,三重,Sanchong,,,,,\r\n雙溪,雙溪,Shuangxi,,,,,\r\n貢寮,貢寮,Gongliao,,,,,\r\n汐止,汐止,Xizhi,,,,,\r\n萬里,萬里,Wanli,,,,,\r\n金山,金山,Jinshan,,,,,\r\n石門,石門,Shimen,,,,,\r\n苓雅,苓雅,Lingya,,,,,\r\n三民,三民,Sanmin,,,,,\r\n新屋,新屋,Xinwu,,,,,\r\n觀音,觀音,Guanyin,,,,,\r\n北竿,北竿,Beigan,,,,,\r\n東引,東引,Dongyin,,,,,\r\n烈嶼,烈嶼,Lieyu,,,,,\r\n旗津,旗津,Qijin,,,,,\r\n長濱,長濱,Changbin,,,,,\r\n豐濱,豐濱,Fengbin,,,,,\r\n霧峰,霧峰,Wufeng,,,,,\r\n大里,大里,Dali,,,,,\r\n烏日,烏日,Wuri,,,,,\r\n中,中,Central,,,,,\r\n南屯,南屯,Nantun,,,,,\r\n西屯,西屯,Xitun,,,,,\r\n北屯,北屯,Beitun,,,,,\r\n潭子,潭子,Tanzi,,,,,\r\n萬華,萬華,Wanhua,,,,,\r\n松山,松山,Songshan,,,,,\r\n士林,士林,Shilin,,,,,\r\n北投,北投,Beitou,,,,,\r\n新城,新城,Xincheng,,,,,\r\n善化,善化,Shanhua,,,,,\r\n山上,山上,Shanshang,,,,,\r\n北斗,北斗,Beidou,,,,,\r\n田尾,田尾,Tianwei,,,,,\r\n金沙,金沙,Jinsha,,,,,\r\n金湖,金湖,Jinhu,,,,,\r\n柳營,柳營,Liuying,,,,,\r\n東山,東山,Dongshan,,,,,\r\n七股,七股,Qigu,,,,,\r\n將軍,將軍,Jiangjun,,,,,\r\n鼓山,鼓山,Gushan,,,,,\r\n左營,左營,Zuoying,,,,,\r\n中壢,中壢,Zhongli,,,,,\r\n寶山,寶山,Baoshan,,,,,\r\n恆春,恆春,Hengchun,,,,,\r\n太平,太平,Taiping,,,,,\r\n鳳林,鳳林,Fenglin,,,,,\r\n萬榮,萬榮,Wanrong,,,,,\r\n龍潭,龍潭,Longtan,,,,,\r\n平鎮,平鎮,Pingzhen,,,,,\r\n南港,南港,Nangang,,,,,\r\n內湖,內湖,Neihu,,,,,\r\n金寧,金寧,Jinning,,,,,\r\n金城,金城,Jincheng,,,,,\r\n尖石,尖石,Jianshi,,,,,\r\n泰安,泰安,Tai'an,,,,,\r\n和平,和平,Heping,,,,,\r\n縣,縣,County,,,,,\r\n鄉,鄉,Township,,,,,\r\n鎮,鎮,Town,,,,,\r\n市,市,City,,,,,\r\n區,區,District,,,,,"
  },
  {
    "path": ".crowdin/strings.pot",
    "content": "#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"\"\n\"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"\"\n\"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"\"\n\"自動定位功能將使用您的裝置上的 GPS，即使 DPIP \"\n\"關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"\"\n\"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) \"\n\"之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"\"\n\"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\"\n\"\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\""
  },
  {
    "path": ".crowdin/weather_station_names.csv",
    "content": "key,zh-Hant,en,ja,ko,ru,vi,zh-Hans\n466850,五分山雷達站,Wufenshan,,,,,\n466881,新北,New Taipei,,,,,\n466900,淡水,Tamsui,,,,,\n466910,鞍部,Anbu,,,,,\n466920,臺北,Taipei,,,,,\n466930,竹子湖,Zhuzihu,,,,,\n466940,基隆,Keelung,,,,,\n466950,彭佳嶼,Pengjiayu,,,,,\n466990,花蓮,Hualien,,,,,\n467050,新屋,Xinwu,,,,,\n467080,宜蘭,Yilan,,,,,\n467110,金門,Kinmen,,,,,\n467270,田中,Tianzhong,,,,,\n467280,後龍,Houlong,,,,,\n467290,古坑,Gukeng,,,,,\n467300,東吉島,Dongjidao,,,,,\n467350,澎湖,Penghu,,,,,\n467410,臺南,Tainan,,,,,\n467420,永康,Yongkang,,,,,\n467441,高雄,Kaohsiung,,,,,\n467480,嘉義,Chiayi,,,,,\n467490,臺中,Taichung,,,,,\n467530,阿里山,Alishan,,,,,\n467540,大武,Dawu,,,,,\n467550,玉山,Yushan,,,,,\n467571,新竹,Hsinchu,,,,,\n467590,恆春,Hengchun,,,,,\n467610,成功,Chenggong,,,,,\n467620,蘭嶼,Lanyu,,,,,\n467650,日月潭,Sun Moon Lake,,,,,\n467660,臺東,Taitung,,,,,\n467790,墾丁雷達站,Kenting,,,,,\n467990,馬祖,Matsu,,,,,\n12J990,口湖工作站,Kouhu Branch Station,,,,,\n12Q970,東港工作站,Donggang Branch,,,,,\n12Q980,恆春工作站,Hengchun Branch,,,,,\n42HA10,萬大發電廠,Wanta Hydro,,,,,\n72AI40,桃改樹林分場,Shulin Sub-station Of Taoyuan ARES,,,,,\n72C440,桃園農改場,,,,,,\n72D080,桃改五峰分場,Wufeng Sub-station Of Taoyuan ARES,,,,,\n72D680,桃改新埔分場,Sinpu Sub-station Of Taoyuan ARES,,,,,\n72G600,臺中農改場,Ta Tsun,,,,,\n72HA00,中改埔里分場,Puli Sub-station Of Taichung ARES,,,,,\n72K220,南改斗南分場,Dounan Sub-station Of Tainan ARES,,,,,\n72M360,南改義竹分場,Yichu Sub-station Of Tainan ARES,,,,,\n72M700,南改鹿草分場,Lucao Sub-station Of Tainan ARES,,,,,\n72N100,臺南農改場,Tainan,,,,,\n72N240,七股研究中心,Qigu Research Center,,,,,\n72Q010,高雄農改場,Pingtung,,,,,\n72S200,東改班鳩分場,Banjuou Sub-station Of Taitung ARES ,,,,,\n72S590,東改賓朗果園,Binlung Orchard Sub-station Of Taitung ARES ,,,,,\n72T250,花蓮農改場,Gi An,,,,,\n72U480,花改蘭陽分場,Lanyang Sub-station Of Hualien ARES,,,,,\n72V140,高改旗南分場,Chinan Sub-station Of Kaohsiung ARES,,,,,\n82A750,茶改北部分場,North Branch Of TBRS,,,,,\n82C160,茶改場,Tea And Beverage Research Station,,,,,\n82H320,茶改中部分場,Middle Branch Of TRES,,,,,\n82H840,茶改南部分場,South Branch Of TBRS,,,,,\n82S580,茶改東部分場,East Branch Of TBRS,,,,,\nA2C560,農工中心,,,,,,\nA2K360,水試臺西試驗場,Taixi Experimental Fishing Ground,,,,,\nA2K630,臺大雲林校區,Yunlin Branch Of NTU,,,,,\nA2N290,臺南蘭花園區,Taiwan Orchid,,,,,\nB2E890,畜試北區分所,North Branch Of TLRI,,,,,\nB2N890,畜產試驗所,Hsin Hua,,,,,\nB2Q810,畜試南區分所,South Branch Of TLRI,,,,,\nB2U990,畜試東區分所,East Branch Of TLRI,,,,,\nC2A540,四堵,Sihdu,,,,,\nC2A560,福山,Fushan,,,,,\nC2A650,火燒寮,Huoshaoliao,,,,,\nC2A660,瑞芳,Rueifang,,,,,\nC2A880,福隆,Fulong,,,,,\nC2A920,富貴角,Fugueijiao,,,,,\nC2C410,中央大學,Ncu,,,,,\nC2D720,關西工作站,Guanxi Experiment Station,,,,,\nC2D730,寶山農場,Baoshan Farm,,,,,\nC2D740,屯原,Tunyuan,,,,,\nC2E520,大湖,Dahu,,,,,\nC2E540,龍溪,Longxi,,,,,\nC2E880,三義,Sanyi,,,,,\nC2E970,八甲,Bajia,,,,,\nC2F000,大肚,Dadu,,,,,\nC2F860,梨山,Lishan,,,,,\nC2F930,大甲,Dajia,,,,,\nC2F990,摩天嶺,Motianling,,,,,\nC2F9A0,中竹林,Zhongzhulin,,,,,\nC2FA00,烏石坑,Wushihkeng,,,,,\nC2FB50,出雲,Chuyun,,,,,\nC2FB60,頭櫃山,Touguishan,,,,,\nC2G640,鹿港,Lukang,,,,,\nC2G840,北斗,Beidou,,,,,\nC2G870,芳苑,Fangyuan,,,,,\nC2G980,田頭村,Toutian Village,,,,,\nC2G9A0,畜試所彰化,Changhua Branch Of TLRI,,,,,\nC2H950,中寮,Zhongliao,,,,,\nC2H9D0,三隻寮,Sanziliao,,,,,\nC2H9E0,國姓南港,Guoxing Nangang,,,,,\nC2H9F0,柑林,Ganlin,,,,,\nC2H9G0,百勝,Bosheng,,,,,\nC2H9H0,苗改南投蜂場,Nantou Bee Farm Of Miaoli TARI,,,,,\nC2H9J0,中台,Zhongtai,,,,,\nC2H9L0,馬烈霸,Malieba,,,,,\nC2H9M0,發祥,Faxiang,,,,,\nC2H9N0,仁愛東眼,Renaidong,,,,,\nC2H9P0,伊拿谷,Yinagu,,,,,\nC2H9Q0,北東眼山,Beidongyanshan,,,,,\nC2H9R0,卓社,Zhuoshe,,,,,\nC2H9S0,龍南,Longnan,,,,,\nC2H9T0,名間竹圍,Mingjianzhuwei,,,,,\nC2H9U0,鳳鵬,Fengpeng,,,,,\nC2H9W0,大坪頂農水,Dapingding Station,,,,,\nC2I090,鳳凰,Fenghuang,,,,,\nC2K240,草嶺,Caoling,,,,,\nC2K610,草嶺石壁,Caolingshibi,,,,,\nC2K620,馬光農場,Maguang Organic Agriculture Circular Park,,,,,\nC2K630,荷苞,Hebao,,,,,\nC2M410,馬頭山,Matoushan,,,,,\nC2M620,瑞里,Ruili,,,,,\nC2M910,嘉義大學,Chiayi University,,,,,\nC2M920,朴子農改,Pozi DARES,,,,,\nC2M930,石卓,Shizhuo,,,,,\nC2M940,日野賀,Riyehe,,,,,\nC2M950,太和,Taihe,,,,,\nC2M960,外寮,Wailiao,,,,,\nC2M970,碧湖,Bihu,,,,,\nC2N160,西拉雅風管處,Siraya NSAH,,,,,\nC2O810,曾文,Cengwen,,,,,\nC2O930,玉井,Yujing,,,,,\nC2O950,安南,Annan,,,,,\nC2R170,屏東,Pingdong,,,,,\nC2R970,屏科大,National Pingtung University,,,,,\nC2V250,甲仙,Jiaxian,,,,,\nC2V260,月眉,Yuemei,,,,,\nC2V310,美濃,Meinong,,,,,\nC2W030,金門農試所,Kimmann,,,,,\nC2W230,畜試所澎湖,Penghu Field Area Of TLRI,,,,,\nCAG100,王功漁港,Wanggong Fishing Port,,,,,\nCAH030,茶改場竹圍站,TRES Chuwei Station,,,,,\nCAJ050,海口故事園區,Haikou Story Camping Park,,,,,\nCAL110,布袋國中,Budai Junior High School,,,,,\nCAN130,水試所海水繁養殖中心,Mariculture Research Center,,,,,\nCAN140,六官養殖協會,Liuguan Aquaculture,,,,,\nCAQ030,崎峰國小,Cifong Elementary School,,,,,\nE2H360,蓮華池,Lienhuchih,,,,,\nE2HA20,林試畢祿溪站,Pilushi,,,,,\nE2K600,四湖植物園,Sihu Botanical Garden,,,,,\nE2P980,林試六龜中心,Lioukuei Research Center ,,,,,\nE2P990,林試扇平站,Shanping,,,,,\nE2S960,林試太麻里2,Taimalee2,,,,,\nE2S980,林試太麻里1,Taimalee Research Center 1,,,,,\nG2AI50,關渡,Guandu,,,,,\nG2F820,農試所(霧峰),Taichung,,,,,\nG2L020,農試嘉義分所,Chiayi Sub-station Of TARI,,,,,\nG2M350,農試溪口農場,Xikou Farm Of TARI,,,,,\nG2P820,農試鳳山分所,Fengshan Tropical Horticultural Of TARI,,,,,\nK2E360,苗栗農改場,,,,,,\nK2E710,苗改生物防治研究中心,Biological Control Branch Of Miaoli TARI,,,,,\nK2F750,種苗改良場,Shin She,,,,,\nU2H480,溪頭,Hsi Tou,,,,,\nU2HA30,臺大和社,NTU Exfohoshe,,,,,\nU2HA40,臺大內茅埔,NTU Exfoneimoupu,,,,,\nU2HA50,臺大竹山,NTU Experimental Forest,,,,,\nV2C250,八德合作社,Bade Cooperative,,,,,\nV2C260,八德蔬果,Bade Fruit And Vegetable,,,,,\nV2K610,大庄合作社,Dazhuang Cooperative,,,,,\nV2K620,麥寮合作社,Mailiao Cooperative,,,,,\nC0A520,山佳,Shanjia,,,,,\nC0A530,坪林,Pinglin,,,,,\nC0A550,泰平,Taiping,,,,,\nC0A570,桶後,Tonghou,,,,,\nC0A640,石碇,Shihding,,,,,\nC0A770,科教館,Science Education Center ,,,,,\nC0A860,大坪,Daping,,,,,\nC0A870,五指山,Wujhihshan,,,,,\nC0A890,雙溪,Shuangsi,,,,,\nC0A931,三和,Sanhe,,,,,\nC0A940,金山,Jinshan,,,,,\nC0A950,鼻頭角,Bitoujiao,,,,,\nC0A970,三貂角,Sandiaojiao,,,,,\nC0A980,社子,Shezih,,,,,\nC0A9C0,天母,Tianmu,,,,,\nC0A9F0,內湖,Neihu,,,,,\nC0AC40,大屯山,Datunshan,,,,,\nC0AC60,三峽,Sanshia,,,,,\nC0AC70,信義,Xinyi,,,,,\nC0AC80,文山,Wenshan,,,,,\nC0ACA0,新莊,Xinzhuang,,,,,\nC0AD10,八里,Bali,,,,,\nC0AD30,蘆洲,Lujhou,,,,,\nC0AD40,土城,Tucheng,,,,,\nC0AD50,鶯歌,Yingge,,,,,\nC0AG80,中和,Zhonghe,,,,,\nC0AH00,汐止,Xizhi,,,,,\nC0AH10,永和,Yonghe,,,,,\nC0AH30,五分山,Wufengshan,,,,,\nC0AH40,平等,Pingdeng,,,,,\nC0AH50,林口,Linkou,,,,,\nC0AH70,松山,Songshan,,,,,\nC0AH80,深坑,Shenkeng,,,,,\nC0AH90,福山植物園,Fushan Botanical Garden,,,,,\nC0AI00,五股,Wugu,,,,,\nC0AI10,屈尺,Quchi,,,,,\nC0AI20,白沙灣,Baishawan,,,,,\nC0AI30,三重,Sanchong,,,,,\nC0AI40,石牌,Shipai,,,,,\nC0AJ20,野柳,Yehliou,,,,,\nC0AJ30,淡水觀海,Danshuei Guanhai,,,,,\nC0AJ40,石門,Shimen,,,,,\nC0AJ50,水湳洞,Shuinandong,,,,,\nC0AJ60,六塊厝,Lioukuaitsuo,,,,,\nC0AJ70,田寮,Tianliao,,,,,\nC0AJ80,板橋,Banchiao,,,,,\nC0AJ90,澳底,Aodi,,,,,\nC0AK10,太平里,Taiping Vil.,,,,,\nC0AK30,硬漢嶺,Yinghanling,,,,,\nC0B010,七堵,Qidu,,,,,\nC0B020,基隆嶼,Keelung Islet,,,,,\nC0B040,大武崙,Dawulun,,,,,\nC0B050,八斗子,Badouzi,,,,,\nC0B060,暖暖,Nuannuan,,,,,\nC0C460,復興,Fuxing,,,,,\nC0C480,桃園,Taoyuan,,,,,\nC0C490,八德,Bade,,,,,\nC0C590,觀音,Guanyin,,,,,\nC0C620,蘆竹,Luzhu,,,,,\nC0C630,大溪,Dasi,,,,,\nC0C650,平鎮,Pingjhen,,,,,\nC0C660,楊梅,Yangmei,,,,,\nC0C670,龍潭,Longtan,,,,,\nC0C680,龜山,Guishan,,,,,\nC0C700,中壢,Zhongli,,,,,\nC0C710,大溪永福,Yongfu Daxi,,,,,\nC0C720,竹圍,Jhuwei,,,,,\nC0C730,中大臨海站,Jhongda Coastal Station,,,,,\nC0C740,觀音工業區,Guanyin Industrial Area,,,,,\nC0C750,新興坑尾,Sinsingkengwei,,,,,\nC0C790,東眼山,Dongyanshan,,,,,\nC0C800,四稜,Sileng,,,,,\nC0D360,梅花,Meihua,,,,,\nC0D430,峨眉,Emei,,,,,\nC0D480,打鐵坑,Datiekeng,,,,,\nC0D540,橫山,Hengshan,,,,,\nC0D550,雪霸,Xueba,,,,,\nC0D560,竹東,Zhudong,,,,,\nC0D580,寶山,Baoshan,,,,,\nC0D590,新豐,Sinfong,,,,,\nC0D650,湖口,Hukou,,,,,\nC0D660,新竹市東區,Dongqu Hsinshu City,,,,,\nC0D670,海天一線,Haitianyisian,,,,,\nC0D680,香山濕地,Siangshan Wetland,,,,,\nC0D690,外湖,Waihu,,,,,\nC0D700,關西,Guanxi,,,,,\nC0D750,樂山林道6k,Leshan 6k,,,,,\nC0D760,大坪苗圃,Daping Nurserygarden,,,,,\nC0E420,竹南,Jhunan,,,,,\nC0E430,南庄,Nanzhuang,,,,,\nC0E550,明德,Mingde,,,,,\nC0E570,白沙屯,Baishatun,,,,,\nC0E590,通霄,Tongxiao,,,,,\nC0E610,馬都安,Madu-An,,,,,\nC0E730,頭份,Toufen,,,,,\nC0E740,造橋,Zaoqiao,,,,,\nC0E750,苗栗,Miaoli,,,,,\nC0E780,銅鑼,Tongluo,,,,,\nC0E791,卓蘭,Zhuolan,,,,,\nC0E810,西湖,Xihu,,,,,\nC0E820,獅潭,Shitan,,,,,\nC0E830,苑裡,Yuanli,,,,,\nC0E850,大河,Dahe,,,,,\nC0E870,高鐵苗栗,THSR Miaoli,,,,,\nC0E910,海埔,Haipu,,,,,\nC0E920,通霄漁港,Tongsiao Fishing Harbor,,,,,\nC0E930,龍鳳,Longfong,,,,,\nC0E940,雪見,Shiuejian,,,,,\nC0E950,松安,Songan,,,,,\nC0E960,觀霧分站,Guanwu,,,,,\nC0F0A0,雪山圈谷,Xueshanjuangu,,,,,\nC0F0B0,石岡,Shigang,,,,,\nC0F0C0,中坑,Zhongkeng,,,,,\nC0F0D0,審馬陣,Shenmazhen,,,,,\nC0F0E0,南湖圈谷,Nanhuquangu,,,,,\nC0F850,東勢,Dongshi,,,,,\nC0F970,大坑,Dakeng,,,,,\nC0F9I0,神岡,Shengang,,,,,\nC0F9K0,大安,Da-An,,,,,\nC0F9L0,后里,Houli,,,,,\nC0F9M0,豐原,Fengyuan,,,,,\nC0F9N0,大里,Dali,,,,,\nC0F9O0,潭子,Tanzi,,,,,\nC0F9P0,清水,Qingshui,,,,,\nC0F9Q0,外埔,Waipu,,,,,\nC0F9R0,龍井,Longjing,,,,,\nC0F9S0,烏日,Wuri,,,,,\nC0F9T0,西屯,Xitun,,,,,\nC0F9U0,南屯,Nantun,,,,,\nC0F9V0,新社,Xinshe,,,,,\nC0F9X0,大雅(中科園區),Daya,,,,,\nC0F9Y0,桃山,Taoshan,,,,,\nC0F9Z0,雪山東峰,Xueshandongfeng,,,,,\nC0FA10,松柏,Songbai,,,,,\nC0FA20,溫寮,Wunliao,,,,,\nC0FA30,梧棲,Wuqi,,,,,\nC0FA40,臺中電廠,Taichung?Power Plant,,,,,\nC0FA50,霧峰,Wufeng,,,,,\nC0FA60,鞍馬山工作站,Anmashan Office,,,,,\nC0FA70,大雪山埡口,Dashiueshan Yakou,,,,,\nC0FA80,小雪山天池,Shiaushiueshan Tianchr,,,,,\nC0FA90,小雪山林道,Shiaushiueshan,,,,,\nC0FB00,大雪山,Dashiueshan,,,,,\nC0FB10,出雲山苗圃,Chuyunshan,,,,,\nC0FB20,雪山橋,Shiueshanchiau,,,,,\nC0FB30,八仙山苗圃,Bashianshan,,,,,\nC0FB40,中橫21.6k,Chungheng 21.6k,,,,,\nC0FB70,大甲溪埔,\"Dajia, Xipu\",,,,,\nC0G620,芬園,Fenyuan,,,,,\nC0G650,員林,Yuanlin,,,,,\nC0G660,溪湖,Xihu,,,,,\nC0G720,溪州,Xizhou,,,,,\nC0G730,二林,Erlin,,,,,\nC0G740,大城,Dacheng,,,,,\nC0G770,福興,Fuxing,,,,,\nC0G780,秀水,Xiushui,,,,,\nC0G800,埔鹽,Puyan,,,,,\nC0G810,埔心,Puxin,,,,,\nC0G820,田尾,Tianwei,,,,,\nC0G830,埤頭,Pitou,,,,,\nC0G860,社頭,Shetou,,,,,\nC0G880,二水,Ershui,,,,,\nC0G890,伸港,Shenggang,,,,,\nC0G900,線西,Xianxi,,,,,\nC0G910,花壇,Huatan,,,,,\nC0G920,永靖,Yongjing ,,,,,\nC0G940,竹塘,Zhutang,,,,,\nC0G950,防潮門,Fangchaomen,,,,,\nC0G960,福寶,Fubao,,,,,\nC0G970,三豐,Sanfong,,,,,\nC0G9B0,和美,Hemei,,,,,\nC0H890,埔里,Puli,,,,,\nC0H960,草屯,Caotun,,,,,\nC0H990,昆陽,Kunyang,,,,,\nC0H9A0,神木村,Shenmu Village,,,,,\nC0H9C0,合歡山,Hehuan Mountain,,,,,\nC0I010,廬山,Lushan,,,,,\nC0I080,信義,Xinyi,,,,,\nC0I110,竹山,Zhushan,,,,,\nC0I360,水里,Shuili,,,,,\nC0I370,魚池,Yuchi,,,,,\nC0I380,集集,Jiji,,,,,\nC0I390,仁愛,Ren'Ai,,,,,\nC0I410,名間,Mingjian,,,,,\nC0I420,國姓,Guoxing,,,,,\nC0I460,南投,Nantou,,,,,\nC0I480,梅峰,Meifeng,,,,,\nC0I490,萬大林道,Wandalindao,,,,,\nC0I520,玉山風口,Yushanfengkou,,,,,\nC0I530,小奇萊,Xiaoqilai,,,,,\nC0I540,奇萊稜線,Qilailengxian,,,,,\nC0K250,崙背,Lunbei,,,,,\nC0K280,四湖,Sihu,,,,,\nC0K291,宜梧,Yiwu,,,,,\nC0K330,虎尾,Huwei,,,,,\nC0K390,土庫,Tuku,,,,,\nC0K400,斗六,Douliu,,,,,\nC0K410,北港,Beigang,,,,,\nC0K420,西螺,Xiluo,,,,,\nC0K430,褒忠,Baozhong,,,,,\nC0K440,二崙,Erlun,,,,,\nC0K450,大埤,Dapi,,,,,\nC0K460,斗南,Dounan,,,,,\nC0K470,林內,Linnei,,,,,\nC0K480,莿桐,Citong,,,,,\nC0K500,元長,Yuanchang,,,,,\nC0K510,水林,Shuilin,,,,,\nC0K530,臺西,Taixi,,,,,\nC0K550,蔦松,Niaosong,,,,,\nC0K560,棋山,Qishan,,,,,\nC0K580,高鐵雲林,THSR Yunlin,,,,,\nC0K590,雲林東勢,\"Dongshi, Yunlin County\",,,,,\nC0K600,箔子寮,Bozihliao,,,,,\nC0M520,東後寮,Donghouliao,,,,,\nC0M530,奮起湖,Fenqihu,,,,,\nC0M640,中埔,Zhongpu,,,,,\nC0M650,朴子,Puzi,,,,,\nC0M660,溪口,Xikou,,,,,\nC0M670,大林,Dalin,,,,,\nC0M680,太保,Taibao,,,,,\nC0M690,水上,Shuishang,,,,,\nC0M700,竹崎,Zhuqi,,,,,\nC0M710,東石,Dongshi,,,,,\nC0M720,番路,Fanlu,,,,,\nC0M730,嘉義市東區,Dongqu Chiayi City,,,,,\nC0M740,六腳,Liujiao,,,,,\nC0M750,布袋,Budai,,,,,\nC0M760,民雄,Minxiong,,,,,\nC0M770,嘉義梅山,Meishan Chiayi County,,,,,\nC0M780,鹿草,Lucao,,,,,\nC0M790,新港,Xingang,,,,,\nC0M800,茶山,Chashan,,,,,\nC0M810,里佳,Lijia,,,,,\nC0M820,達邦,Dabang,,,,,\nC0M850,表湖,Biaohu,,,,,\nC0M860,新美,Shinmei,,,,,\nC0M880,好美里,Haomeili,,,,,\nC0N010,鯤鯓國小,Kunshen Elementary School,,,,,\nC0N020,城西,Chengsi,,,,,\nC0N030,四草,Sihtsao,,,,,\nC0N040,蘆竹溝,Lujhugou,,,,,\nC0N050,蚵寮,Eliao,,,,,\nC0O830,北寮,Beiliao,,,,,\nC0O840,王爺宮,Wangyegong,,,,,\nC0O860,大內,Danei,,,,,\nC0O900,善化,Shanhua,,,,,\nC0O960,崎頂,Qiding,,,,,\nC0O970,虎頭埤,Hutoupi,,,,,\nC0O980,新市,Xinshi,,,,,\nC0O990,媽廟,Mamiao,,,,,\nC0R100,尾寮山,Weiliaoshan,,,,,\nC0R130,阿禮,Ali,,,,,\nC0R140,瑪家,Majia,,,,,\nC0R150,三地門,Sandimen,,,,,\nC0R160,鹽埔,Yanpuxinwei,,,,,\nC0R190,赤山,Chishan,,,,,\nC0R220,潮州,Chaojhou,,,,,\nC0R240,來義,Laiyi,,,,,\nC0R260,春日,Chunri,,,,,\nC0R270,琉球嶼,Liouciouyu,,,,,\nC0R280,檳榔,Binlang,,,,,\nC0R320,車城,Checheng,,,,,\nC0R341,牡丹,Mudan,,,,,\nC0R350,貓鼻頭,Maobitou,,,,,\nC0R440,大漢山,Dahanshan,,,,,\nC0R470,高樹,Gaoshu,,,,,\nC0R480,長治,Changzhi,,,,,\nC0R490,九如,Jiuru,,,,,\nC0R520,崁頂,Kanding,,,,,\nC0R540,佳冬,Jiadong,,,,,\nC0R550,新埤,Xinpi,,,,,\nC0R560,新園,Xinyuan,,,,,\nC0R570,麟洛,Linluo,,,,,\nC0R580,南州,Nanzhou,,,,,\nC0R590,里港,Ligang,,,,,\nC0R600,舊泰武,Jiutaiwu,,,,,\nC0R620,墾雷,Kenlei,,,,,\nC0R640,東港,Donggang,,,,,\nC0R650,竹田,Zhutian,,,,,\nC0R660,枋寮,Fangliao,,,,,\nC0R670,楓港,Fenggang,,,,,\nC0R680,佳樂水,Jialeshui,,,,,\nC0R690,墾丁,Kenting,,,,,\nC0R700,枋山,Fangshan,,,,,\nC0R710,龍磐,Longpan,,,,,\nC0R720,旭海,Xuhai,,,,,\nC0R730,大坪頂,Dapingding,,,,,\nC0R741,獅子,Shizi,,,,,\nC0R750,四林格山,Silingeshan,,,,,\nC0R760,南仁湖,Nanrenhu,,,,,\nC0R770,保力,Baoli,,,,,\nC0R780,滿州,Manzhou,,,,,\nC0R790,九棚,Jiupeng,,,,,\nC0R800,丹路,Danlu,,,,,\nC0R810,內獅,Neishi,,,,,\nC0R820,白鷺,Bailu,,,,,\nC0R830,高士,Gaoshi,,,,,\nC0R840,牡丹池山,Mudanchisahn,,,,,\nC0R850,林邊,Linbian,,,,,\nC0R860,鼻頭,Bitou,,,,,\nC0R870,興海,Singhai,,,,,\nC0R880,後壁湖,Houbihu,,,,,\nC0R890,山海,Shanhai,,,,,\nC0R900,竹坑,Jhukeng,,,,,\nC0R910,下寮,Sialiao,,,,,\nC0R920,塭仔,Wunzai,,,,,\nC0R930,萬丹,Wandan,,,,,\nC0R940,加祿堂,Jialutang,,,,,\nC0R950,萬隆國小,Wanlongguoxiao,,,,,\nC0R960,內埔,Neipu,,,,,\nC0S660,下馬,Xiama,,,,,\nC0S690,太麻里,Taimali,,,,,\nC0S700,知本,Jhihben,,,,,\nC0S710,鹿野,Luye,,,,,\nC0S730,綠島,Ludao,,,,,\nC0S740,池上,Chihshang,,,,,\nC0S750,向陽,Siangyang,,,,,\nC0S760,紅石,Hongshih,,,,,\nC0S770,大溪山,Dasishan,,,,,\nC0S790,金崙,Jinlun,,,,,\nC0S810,東河,Donghe,,,,,\nC0S830,長濱,Changbin,,,,,\nC0S840,南田,Nantian,,,,,\nC0S890,關山,Guanshan,,,,,\nC0S900,蘭嶼高中,Lanyu High School,,,,,\nC0S910,蘭嶼燈塔,Lanyu Lighthouse,,,,,\nC0S920,金峰嘉蘭,Jialan Jinfeng,,,,,\nC0S930,延平,Yanping,,,,,\nC0S940,石寧山,Shiningshan,,,,,\nC0S950,七塊厝,Qikuaicuo,,,,,\nC0S960,香蘭,Xianglan,,,,,\nC0S970,加津林,Jiajinlin,,,,,\nC0S980,勝林山,Shenglinshan,,,,,\nC0S990,山豬窟,Shanzhuku,,,,,\nC0SA00,歷坵,Liqiu,,,,,\nC0SA10,檳榔四格山,Binlangsigeshan,,,,,\nC0SA20,金崙山,Jinlunshan,,,,,\nC0SA30,都歷,Duli,,,,,\nC0SA40,瑞和,Ruihe,,,,,\nC0SA60,知本（水試所）,Zhiben (FRI),,,,,\nC0SA80,土坂,Tuban,,,,,\nC0SA90,達仁林場,Darenlinchang,,,,,\nC0SB10,美和,Meihe,,,,,\nC0SB20,富岡,Fugang,,,,,\nC0SB30,新蘭,Dulan Fire Brigade,,,,,\nC0SB40,興隆, Xinglong,,,,,\nC0SB50,叭嗡嗡,Baweng,,,,,\nC0SB60,白守蓮,Baishoulian,,,,,\nC0SB70,小港漁港,Xiaogang Fishing Harbor,,,,,\nC0SB80,長濱漁港,Changbin Fishing Harbor,,,,,\nC0T790,大禹嶺,Dayuling,,,,,\nC0T820,天祥,Tianxiang,,,,,\nC0T870,鯉魚潭,Liyutan,,,,,\nC0T900,西林,Xilin,,,,,\nC0T960,光復,Guangfu,,,,,\nC0T9A0,月眉山,Yuemeishan,,,,,\nC0T9B0,水源,Shuiyuan,,,,,\nC0T9D0,和中,Hezhong,,,,,\nC0T9E0,大坑,Dakeng,,,,,\nC0T9F0,水璉,Shuilian,,,,,\nC0T9G0,鳳林山,Fenglinshan,,,,,\nC0T9H0,加路蘭山,Jialulanshan,,,,,\nC0T9I0,豐濱,Fengbin,,,,,\nC0T9M0,靜浦,Jingpu,,,,,\nC0T9N0,富里,Fuli,,,,,\nC0TA10,花蓮漁港,Hualien Fishing Harbor,,,,,\nC0TA20,加灣,Jiawan,,,,,\nC0TA30,鹽寮,Yanliao,,,,,\nC0TA40,秀林,Xiulin,,,,,\nC0TA50,和仁,Heren,,,,,\nC0TA80,立霧山,Liwushan,,,,,\nC0U520,雙連埤,Shuanglianpi,,,,,\nC0U600,礁溪,Chiaoshi,,,,,\nC0U650,玉蘭,Yulan,,,,,\nC0U710,太平山,Taipingshan,,,,,\nC0U720,南山,Nanshan,,,,,\nC0U750,龜山島,Gueishandao,,,,,\nC0U760,東澳,Dong-Ao,,,,,\nC0U770,南澳,Nanao,,,,,\nC0U780,五結,Wujie,,,,,\nC0U860,頭城,Toucheng,,,,,\nC0U870,大礁溪,Dajiaoxi,,,,,\nC0U880,北關,Beiguan,,,,,\nC0U890,三星,Sanxing,,,,,\nC0U900,內城,Neicheng,,,,,\nC0U910,冬山,Dongshan,,,,,\nC0U940,羅東,Luodong,,,,,\nC0U950,鶯子嶺,Yingziling,,,,,\nC0U960,翠峰湖,Cuifenghu,,,,,\nC0U970,大福,Dafu,,,,,\nC0U980,坪林石牌,Shipai Pinglin,,,,,\nC0U990,員山,Yuanshan,,,,,\nC0UA00,土場,Tuchang,,,,,\nC0UA10,鴛鴦湖,Yuanyanghu,,,,,\nC0UA20,多加屯,Duojiatun,,,,,\nC0UA30,白嶺,Bailing,,,,,\nC0UA40,西德山,Xideshan,,,,,\nC0UA50,西帽山,Ximaoshan,,,,,\nC0UA60,樟樹山,Zhangshushan,,,,,\nC0UA70,桃源谷,Taoyuangu,,,,,\nC0UA80,大溪漁港,Dasi Fishing Harbor,,,,,\nC0UA90,石城,Shihcheng,,,,,\nC0UB00,淡江大學蘭陽校園,Tamkang Lanyang Campus,,,,,\nC0UB10,蘇澳,Suao,,,,,\nC0UB20,壯圍,Jhuangwei,,,,,\nC0UB60,明池,Mingchr,,,,,\nC0UB70,太平山中間站,Jhongjian,,,,,\nC0UB80,翠峰林道6K,Trifong 6k,,,,,\nC0UB90,太平山莊,Taipingshan Villa,,,,,\nC0V210,復興,Fuxing,,,,,\nC0V350,溪埔,Xipu,,,,,\nC0V360,內門,Neimen,,,,,\nC0V370,古亭坑,Gutingkeng,,,,,\nC0V400,阿公店,Agongdian,,,,,\nC0V440,鳳山,Fengshan,,,,,\nC0V450,鳳森,Fengsen,,,,,\nC0V490,新興,Sinsing,,,,,\nC0V530,阿蓮,Alian,,,,,\nC0V610,梓官,Ziguan,,,,,\nC0V620,永安,Yong'An,,,,,\nC0V630,茄萣,Qieding,,,,,\nC0V640,湖內,Hunei,,,,,\nC0V650,彌陀,Mituo,,,,,\nC0V660,岡山,Gangshan,,,,,\nC0V680,仁武,Renwu,,,,,\nC0V690,鼓山,Gushan,,,,,\nC0V700,三民,Sanmin,,,,,\nC0V710,苓雅,Lingya,,,,,\nC0V720,林園,Linyuan,,,,,\nC0V730,大寮,Daliao,,,,,\nC0V740,旗山,Qishan,,,,,\nC0V750,路竹,Luzhu,,,,,\nC0V760,橋頭,Qiaotou,,,,,\nC0V770,大社,Dashe,,,,,\nC0V790,萬山,Wanshan,,,,,\nC0V800,六龜,Liugui,,,,,\nC0V810,左營,Zuoying,,,,,\nC0V820,小林,Xiaolin,,,,,\nC0V840,鳳鼻頭,Fongbitou,,,,,\nC0V850,蚵仔寮,Kezailiao,,,,,\nC0V860,南寮,Nanliao,,,,,\nC0V870,文安,Wunan,,,,,\nC0V880,興達,Singda,,,,,\nC0V890,前鎮,Chian Jhen,,,,,\nC0V900,汕尾,Shanwei,,,,,\nC0V910,大樹,Dashu,,,,,\nC0W110,東莒,Dongju,,,,,\nC0W120,西嶼,Xiyu,,,,,\nC0W130,花嶼,Huayu,,,,,\nC0W140,金沙,Jinsha ,,,,,\nC0W150,金寧,Jinning,,,,,\nC0W160,烏坵,Wuqiu,,,,,\nC0W180,七美,Qimei,,,,,\nC0W190,望安,Wangan,,,,,\nC0W200,湖西,Husi,,,,,\nC0W220,北竿,Beigan,,,,,\nC0W240,九宮,Jiugong ,,,,,\nC0X050,東河,Donghe,,,,,\nC0X060,下營,Xiaying,,,,,\nC0X080,佳里,Jiali,,,,,\nC0X100,臺南市北區,Beiqu Tainan City,,,,,\nC0X110,臺南市南區,Nanqu Tainan City,,,,,\nC0X120,麻豆,Madou,,,,,\nC0X130,官田,Guantian,,,,,\nC0X140,西港,Xigang,,,,,\nC0X150,安定,Anding,,,,,\nC0X160,仁德,Rende,,,,,\nC0X170,關廟,Guanmiao,,,,,\nC0X180,山上,Shanshang,,,,,\nC0X190,安平,Anping,,,,,\nC0X200,左鎮,Zuozhen,,,,,\nC0X210,白河,Baihe,,,,,\nC0X220,學甲,Xuejia,,,,,\nC0X230,鹽水,Yanshui,,,,,\nC0X240,關子嶺,Guanziling,,,,,\nC0X250,新營,Xinying,,,,,\nC0X260,後壁,Houbi,,,,,\nC0X280,將軍,Jiangjun,,,,,\nC0X290,北門,Beimen,,,,,\nC0X300,鹿寮,Luliao,,,,,\nC0X320,柳營,Liuying,,,,,\nC0Z020,明里,Mingli,,,,,\nC0Z050,佳心,Jiaxin,,,,,\nC0Z061,玉里,Yuli,,,,,\nC0Z070,舞鶴,Wuhe,,,,,\nC0Z080,富源,Fuyuan,,,,,\nC0Z100,東華,Donghwa,,,,,\nC0Z150,吉安光華,Guanghua Ji-An,,,,,\nC0Z160,鳳林,Fenglin,,,,,\nC0Z170,卓溪,Zhuoxi,,,,,\nC0Z180,新城,Xincheng,,,,,\nC0Z190,富世,Fushi,,,,,\nC0Z200,萬榮,Wanrong,,,,,\nC0Z210,瑞穗,Ruisui,,,,,\nC0Z220,和平林道,Hepinglindao,,,,,\nC0Z230,和平,Heping,,,,,\nC0Z250,瑞穗林道,Ruisuilindao,,,,,\nC0Z270,蕃薯寮,Fanshuliao,,,,,\nC0Z280,德武,Dewu,,,,,\nC0Z290,赤柯山,Chikeshan,,,,,\nC0Z300,東里,Dongli,,,,,\nC0Z310,清水斷崖,Qingshui Cliff,,,,,\nC0Z320,清水林道,Qingshuilindao,,,,,\nC0Z330,安通山,Antongshan,,,,,\nC1A630,下盆,Siapen,,,,,\nC1A750,石碇服務區,Shiding Service Area,,,,,\nC1A760,坪林交控,Pinglin Traffic Control Center,,,,,\nC1A9N0,四十份,Sihshihfen,,,,,\nC1AC50,關渡,Guandu,,,,,\nC1AI50,國三N016K,Freeway No. 3 - Rain - N016k,,,,,\nC1AI60,國一39K邊坡,Freeway No. 1 - Rain – N039k,,,,,\nC1C510,水尾,Shueiwei,,,,,\nC1D380,新埔,Sinpu,,,,,\nC1D400,鳥嘴山,Niaozueishan,,,,,\nC1D410,白蘭,Bailan,,,,,\nC1D420,太閣南,Taigenan,,,,,\nC1D630,飛鳳山,Fei Feng Mountain,,,,,\nC1D640,外坪(五指山),Waiping(Wuzhihshan),,,,,\nC1E451,象鼻,Xiangbi,,,,,\nC1E461,松安,Song-An,,,,,\nC1E480,鳳美,Fongmei,,,,,\nC1E511,新開,Xinkai,,,,,\nC1E601,南勢,Nanshi,,,,,\nC1E670,南礦,Nankuang,,,,,\nC1E681,南勢山,Nanshishan,,,,,\nC1E691,南湖,Nanhu,,,,,\nC1E701,八卦,Bagua,,,,,\nC1E711,馬拉邦山,Malabangshan,,,,,\nC1E721,泰安,Tai-An,,,,,\nC1E770,公館,Gongguan,,,,,\nC1E890,國三N149K,Freeway No. 1 - Rain – N149k,,,,,\nC1E900,國一N128K,Freeway No. 1 - Rain – N128k,,,,,\nC1F871,上谷關,Shangguguan,,,,,\nC1F891,稍來,Shaolai,,,,,\nC1F911,新伯公,Xinbogong,,,,,\nC1F941,雪嶺,Xueling,,,,,\nC1F9B1,桐林,Tonglin,,,,,\nC1F9C1,白冷,Baileng,,,,,\nC1F9D1,白毛台,Baimaotai,,,,,\nC1F9E1,龍安,Long-An,,,,,\nC1F9F1,伯公龍,Bogonglong,,,,,\nC1F9G1,慶福山,Cingfushan,,,,,\nC1F9J1,清水林,Qingshuilin,,,,,\nC1F9W0,德基,Deji,,,,,\nC1G691,下水埔,Xiashuipu,,,,,\nC1G9D0,國一S218K,Freeway No. 1 - Rain – S218k,,,,,\nC1H000,翠峰,Cuifeng,,,,,\nC1H840,國三N238K,Freeway No. 3 - Rain –N238k,,,,,\nC1H900,清流,Qingliu,,,,,\nC1H920,長豐,Changfeng,,,,,\nC1H941,雙冬,Shuangdong,,,,,\nC1H971,六分寮,Liufenliao,,,,,\nC1H9B1,阿眉,Amei,,,,,\nC1I020,萬大,Wanda,,,,,\nC1I030,武界,Wujie,,,,,\nC1I050,丹大,Danda,,,,,\nC1I070,和社,Heshe,,,,,\nC1I101,溪頭,Xitou,,,,,\nC1I121,大鞍,Da-An,,,,,\nC1I131,桶頭,Tongtou,,,,,\nC1I140,卡奈托灣,Kanaituowan,,,,,\nC1I150,青雲,Qingyun,,,,,\nC1I201,中心崙,Zhongxinlun,,,,,\nC1I211,蘆竹湳,Luzhunan,,,,,\nC1I220,樟湖,Zhanghu,,,,,\nC1I230,九份二山,Jiufen'Ershan,,,,,\nC1I240,外大坪,Waidaping,,,,,\nC1I250,鯉潭,Litan,,,,,\nC1I260,北坑,Beikeng,,,,,\nC1I280,埔中,Puzhong,,,,,\nC1I290,豐丘,Fengqiu,,,,,\nC1I310,西巒,Xiluan,,,,,\nC1I320,奧萬大,Aowanda,,,,,\nC1I330,楓樹林,Fengshulin,,,,,\nC1I340,新興橋,Xinxingqiao,,,,,\nC1I400,凌霄,Lingxiao,,,,,\nC1I430,翠華,Cuihua,,,,,\nC1I440,新高口,Xingaokou,,,,,\nC1I450,望鄉山,Wangxiangshan,,,,,\nC1I470,杉林溪,Shanlinxi,,,,,\nC1I500,大尖山,Dajianshan,,,,,\nC1I510,線浸林道,Xianjinlindao,,,,,\nC1I550,國六W023K,Freeway No. 6 - Rain – W023k,,,,,\nC1K540,口湖,Kouhu,,,,,\nC1M390,龍美,Longmei,,,,,\nC1M400,菜瓜坪,Caiguaping,,,,,\nC1M480,獨立山,Dulishan,,,,,\nC1M600,頭凍,Toudong,,,,,\nC1M610,石磐龍,Shipanlong,,,,,\nC1M640,十字,Shizi,,,,,\nC1M870,國三N285K,Freeway No. 3 - Rain –N285k,,,,,\nC1N001,沙崙,Shalun,,,,,\nC1O850,環湖,Huanhu,,,,,\nC1O870,大棟山,Dadongshan,,,,,\nC1O880,關山,Guanshan,,,,,\nC1O921,楠西,Nanxi,,,,,\nC1O940,東山服務區,Dongshan Service Area,,,,,\nC1R110,口社,Gusia,,,,,\nC1R120,上德文,Shangdewun,,,,,\nC1R250,力里,Lili,,,,,\nC1R290,石門山,Shihmenshan,,,,,\nC1R610,西大武山,Xidawushan,,,,,\nC1R630,龍泉,Longquan,,,,,\nC1S670,摩天,Motian,,,,,\nC1S800,華源,Huayuan,,,,,\nC1S820,金峰,Jinfeng,,,,,\nC1S850,豐南,Funan,,,,,\nC1S860,利嘉,Lichai,,,,,\nC1S870,南美山,Nanmaisan,,,,,\nC1S880,壽卡,Shouka,,,,,\nC1SA50,利嘉林道,Lijialindao,,,,,\nC1SA70,都蘭,Dulan,,,,,\nC1T800,洛韶,Luoshao,,,,,\nC1T810,慈恩,Ci-En,,,,,\nC1T830,布洛灣,Buluowan,,,,,\nC1T920,中興,Zhongxing,,,,,\nC1T940,大觀,Daguan,,,,,\nC1T950,太安,Tai-An,,,,,\nC1T970,大農,Danong,,,,,\nC1T980,龍澗,Longjian,,,,,\nC1T990,高寮,Gaoliao,,,,,\nC1TA00,太魯閣,Taroko,,,,,\nC1U501,牛鬥,Nioudou,,,,,\nC1U670,寒溪,Hanxi,,,,,\nC1U840,東澳嶺,Dongaoling,,,,,\nC1U850,觀音海岸,Guanyin Coast,,,,,\nC1U920,思源,Siyuan,,,,,\nC1U930,粉鳥林,Fenniaolin,,,,,\nC1V160,達卡努瓦,Dakanuwa,,,,,\nC1V170,排雲,Paiyun,,,,,\nC1V190,南天池,Nantianchi,,,,,\nC1V200,梅山,Meishan,,,,,\nC1V220,小關山,Xiaoguanshan,,,,,\nC1V231,高中,Gaozhong,,,,,\nC1V300,御油山,Yuyoushan,,,,,\nC1V340,大津,Dajin,,,,,\nC1V390,尖山,Jianshan,,,,,\nC1V570,吉東,Jiadong,,,,,\nC1V580,溪南(特生中心),Xinan,,,,,\nC1V590,新發,Xinfa,,,,,\nC1V600,藤枝,Tengzhi,,,,,\nC1V780,多納林道,Duonalindao,,,,,\nC1V830,國三S383K,Freeway No. 3 - Rain – S383k,,,,,\nC1X040,東原,Dongyuan,,,,,\nC1Z030,紅葉,Hongye,,,,,\nC1Z040,立山,Lishan,,,,,\nC1Z110,三棧,Sanzhan,,,,,\nC1Z120,壽豐,Shoufeng,,,,,\nC1Z130,銅門,Tongmen,,,,,\nC1Z140,荖溪,Laoxi,,,,,\nC1Z240,中平林道,Zhongpinglindao,,,,,"
  },
  {
    "path": ".cursorrules",
    "content": "# Cursor AI Rules\n\nLast Modified: 2025-12-29\n\nCheck for staleness: If this file is older than 2 months, ask the user to confirm the instructions are still valid before proceeding.\n\n## Communication Style\n\nMake responses concise and only reply with necessary information. Avoid verbose explanations unless specifically requested.\n\n## Git Workflow\n\n- Check worktree status before any work: Refuse to process any prompts if the git worktree is not clean. Ask the user to commit or stash changes first.\n- Automatically commit any edits done by AI with AI co-authorship:\n  Co-Authored-By: Cursor <cursor@cursor.com>\n\n## AI Usage Policy\n\n**IMPORTANT:** AI usage is **prohibited** in this repository except for **documentation purposes only**. All documentation must follow Effective Dart (https://dart.dev/effective-dart) style guidelines.\n\n- Do NOT use AI to generate or modify code\n- Documentation is the ONLY acceptable use case for AI\n- All documentation must adhere to Effective Dart conventions\n\n## Project: DPIP\n\nFlutter-based disaster prevention app for earthquake early warnings and weather alerts.\n- Target: Android/iOS\n- Flutter >=3.38.0, Dart >=3.10.0\n- State Management: Provider\n- Routing: go_router (type-safe)\n\n## Essential Commands\n\n```bash\n# Install dependencies\nflutter pub get --no-example\n\n# Generate code (after model/route changes)\ndart run build_runner build\n\n# Build\nflutter build apk --release\nflutter build ios --release\n```\n\n## Code Standards\n\n- Lint: package:lint/strict.yaml\n- Format: 80 char width, preserve trailing commas\n- public_member_api_docs: warning level\n- Generated files (**.g.dart, **.freezed.dart) excluded from analysis\n\n## Architecture\n\n### Global State (lib/core/providers.dart)\n- DpipDataModel - Earthquake/weather data\n- SettingsLocationModel - Location preferences\n- SettingsMapModel - Map settings\n- SettingsNotificationModel - Notification config\n- SettingsUserInterfaceModel - UI/i18n\n\n### Key Directories\n- lib/api/ - API client, models (freezed/json_annotation)\n- lib/app/ - Feature modules by screen\n- lib/core/ - Services (FCM, GPS, notifications, EEW logic)\n- lib/models/ - ChangeNotifier models\n- lib/utils/ - Extensions and helpers\n- lib/widgets/ - Reusable components\n\n### Code Generation\nRun `dart run build_runner build` after changes to:\n- router.dart (go_router_builder)\n- @JsonSerializable() models\n- @freezed classes\n\n## Important Notes\n\n- No test suite exists\n- Settings paths use numbered prefixes: (1.eew), (2.earthquake), etc.\n- API uses load balancing across multiple endpoints\n- Compressed assets: *.json.gz files decompressed at runtime\n- Localizations: i18n_extension with Crowdin integration\n- Map: MapLibre GL with multiple layer managers\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "/.github/ @exptechtw/dpip-developers\n\n/lib/app/ @kamiya10\n/lib/models/ @kamiya10\n/lib/core/service.dart @kamiya10\n/tools/update_translations.sh @kamiya10\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: exptech # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug.yml",
    "content": "name: \"🚧問題回報\"\nlabels: [\"錯誤\"]\nprojects: [\"ExpTechTW/12\"]\ndescription: |\n  請詳細描述你遇到的問題協助我們排除問題\ntitle: '[Bug]: '\nbody:\n- type: dropdown\n  id: platform\n  attributes:\n    label: 平台\n    options:\n    - iOS\n    - Android\n    default: 0\n  validations:\n    required: true\n- type: input\n  attributes:\n    label: 版本\n    description: 填入版本\n  validations:\n    required: true\n- type: input\n  attributes:\n    label: 裝置\n    description: 填入裝置名稱\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: 內文\n    description: 請輸入內容\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: 重現步驟\n    description: 如果有的話請輸入重現步驟\n    placeholder: |\n      1. ...\n      2. ...\n      3. ...\n  validations:\n    required: false\n- type: textarea\n  attributes:\n    label: 影片/照片\n    description: 如果有的話請上傳影片或照片\n  validations:\n    required: false\n- type: textarea\n  attributes:\n    label: 日誌\n    description: 如果有的話請輸入日誌內容\n  validations:\n    required: false\n- type: markdown\n  attributes:\n    value: \"### 此檔案由 ExpTech Studio 設計製作\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Idea.md",
    "content": "---\nname: \"\\U0001F50C想法補充\"\nabout: 快來說說你的想法吧\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### 🌟想法補充\n* 補充說明:\n\n#### 此檔案由 ExpTech.tw 設計製作\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\n  如果是正在進行中的 Pull Request，請使用草稿 PR 功能，\n  詳情請參見：https://github.blog/2019-02-14-introducing-draft-pull-requests/\n\n  為了能更即時地審核/回應，請避免強制推送其他 commit 至已經收到 Review 的 PR。\n\n  在提交 Pull Request 之前，請確保您已完成以下事項：\n  - 👷‍♀️ 在大多數情況下，建立小型的 PR 是可以的。\n  - ✅ 為您的更改提供測試。\n  - 📝 使用具有描述性的 commit 訊息。\n  - 📗 更新相關技術文件並包含相關截圖。\n-->\n\n## 這是什麼類型的 PR？\n\n> 選擇所有符合的項目\n\n- [ ] 重構\n- [ ] 新功能\n- [ ] 錯誤修復\n- [ ] 最佳化\n- [ ] 技術文件更新\n\n## 描述\n\n<!-- 請在這裡描述這個 PR 修改的內容 -->\n\n## 相關 issue\n\n<!--\n對於與某個問題相關或關閉某個問題的 Pull Request，請在下方註記它們。\n我們遵循 Github 的 [將問題鏈接到 Pull Request](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) 指南。\n\n例如，「closes #1234」將把目前的 PR 與 issue#1234 連接起來。\n當我們合併 PR 時，Github 就會自動關閉對應的 issue 。\n-->\n\n- 相關問題 #\n- closes #\n\n## QA 指南、截圖、錄像\n\n> _請將這行替換成：如何測試您的 PR 的步驟，已測試的裝置註釋，以及任何相關的 UI 更改圖片。_\n\n### UI 無障礙清單\n\n> _如果您的 PR 包含 UI 更改，請使用此清單：_\n\n- [ ] 變數名稱實現語意化命名？\n- [ ] 測試通過 AA 顏色對比？\n"
  },
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Build Android\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  changes:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    outputs:\n      android: ${{ steps.filter.outputs.android }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dorny/paths-filter@v3\n        id: filter\n        with:\n          filters: |\n            android:\n              - 'android/**'\n              - 'assets/**'\n              - 'lib/**'\n              - 'pubspec.yaml'\n\n  Android:\n    needs: changes\n    if: ${{ needs.changes.outputs.android == 'true' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Java\n        uses: actions/setup-java@v4\n        with:\n          distribution: \"temurin\"\n          java-version: \"25\"\n          cache: \"gradle\"\n\n      - name: Setup Flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          cache: true\n\n      - name: Cache Gradle\n        uses: actions/cache@v3\n        env:\n          cache-name: cache-gradle\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/wrapper/gradle-wrapper.properties', '**/build.gradle') }}\n          restore-keys: |\n            ${{ runner.os }}-build-${{ env.cache-name }}-\n            ${{ runner.os }}-build-\n\n      - name: Cache Android NDK\n        uses: actions/cache@v4\n        with:\n          path: |\n            /usr/local/lib/android/sdk/ndk/27.0.12077973\n            /usr/local/lib/android/sdk/ndk/28.2.13676358\n          key: ${{ runner.os }}-ndk-27-28\n\n      - name: Cache Android sdk\n        uses: actions/cache@v3\n        env:\n          cache-name: cache-Android-sdk\n        with:\n          path: |\n            /usr/local/lib/android/sdk/platforms/android-31\n            /usr/local/lib/android/sdk/platforms/android-33\n          key: ${{ runner.os }}-android-platforms-${{ env.cache-name }}-${{ hashFiles('android/gradle.properties') }}\n          restore-keys: |\n            ${{ runner.os }}-build-${{ env.cache-name }}-\n            ${{ runner.os }}-build-\n\n      - name: Install dependencies and run build_runner\n        run: |\n          flutter pub get\n          dart run build_runner build --delete-conflicting-outputs\n\n      - name: Decode keystore\n        run: |\n          echo \"${{ secrets.KEYSTORE_BASE64 }}\" | base64 --decode > android/app/my-release-key.jks\n\n      - name: Create key.properties\n        run: |\n          cat > android/key.properties << EOF\n          storePassword=${{ secrets.KEYSTORE_PASSWORD }}\n          keyPassword=${{ secrets.KEY_PASSWORD }}\n          keyAlias=${{ secrets.KEY_ALIAS }}\n          storeFile=my-release-key.jks\n          EOF\n\n      - name: Build Release APK\n        run: flutter build apk --release --target-platform android-arm64,android-x64\n\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: DPIP-Android-Release\n          path: build/app/outputs/flutter-apk/*.apk\n          retention-days: 7\n          compression-level: 6\n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened]\n    paths:\n      - '.crowdin/**'\n      - 'android/**'\n      - 'assets/**'\n      - 'ios/**'\n      - 'lib/**'\n      - 'tools/**'\n      - 'analysis_options.yaml'\n      - 'crowdin.yml'\n      - 'pubspec.yaml'\n      - 'CHANGELOG.md'\n      - 'README.md'\n    branches:\n      - '**'\n      - '!i18n'\n\njobs:\n  claude-review:\n    runs-on: ubuntu-latest\n    environment: pr-actions\n    permissions:\n      contents: read\n      pull-requests: write\n      id-token: write\n    \n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          track_progress: true\n          prompt: |\n            Please review this pull request and provide feedback on:\n            - Code quality and best practices\n            - Potential bugs or issues\n            - Performance considerations\n            - Security concerns\n            - Test coverage\n            \n            This is a flutter project, please review the PR with flutter best practices in mind.\n            Be constructive and helpful in your feedback.\n            Please suggest changes in the PR review comments if there are any important issues or improvements.\n\n            Note: The PR Branch is already checked out.\n          \n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options\n          claude_args: '--allowed-tools \"Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)\"'\n\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          # prompt: 'Update the pull request description to include a summary of changes.'\n\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options\n          # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'\n"
  },
  {
    "path": ".github/workflows/ios.yml",
    "content": "name: Build iOS\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  changes:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    outputs:\n      ios: ${{ steps.filter.outputs.ios }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dorny/paths-filter@v3\n        id: filter\n        with:\n          filters: |\n            ios:\n              - 'ios/**'\n              - 'assets/**'\n              - 'lib/**'\n              - 'pubspec.yaml'\n\n  iOS:\n    needs: changes\n    if: ${{ needs.changes.outputs.ios == 'true' }}\n    runs-on: macos-latest\n    permissions:\n      contents: read\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          cache: true\n\n      - name: Cache CocoaPods\n        uses: actions/cache@v4\n        with:\n          path: |\n            ios/Pods\n            ~/Library/Caches/CocoaPods\n          key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-pods-\n\n      - name: Install dependencies and run build_runner\n        run: |\n          flutter pub get\n          dart run build_runner build --delete-conflicting-outputs\n\n      - name: Install iOS Pods\n        working-directory: ios\n        run: pod install --repo-update\n\n      - name: Build iOS App and create IPA\n        run: |\n          flutter build ios --debug --no-codesign\n          mkdir -p Payload\n          cp -R build/ios/iphoneos/Runner.app Payload/Runner.app\n          zip -qr DPIP.ipa Payload\n\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: DPIP-iOS\n          path: |\n            build/ios/iphoneos/Runner.app\n            DPIP.ipa\n          retention-days: 7\n          compression-level: 6\n"
  },
  {
    "path": ".github/workflows/lock.yml",
    "content": "# Configuration for Lock Threads - https://github.com/dessant/lock-threads\n\nname: \"Lock Threads\"\n\n# By specifying the access of one of the scopes, all of those that are not\n# specified are set to 'none'.\npermissions:\n  issues: write\n\non:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  lock:\n    permissions:\n      issues: write\n    runs-on: ubuntu-latest\n    steps:\n      - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771\n        with:\n          process-only: \"issues\"\n          github-token: ${{ github.token }}\n          # Number of days of inactivity before a closed issue is locked.\n          issue-inactive-days: \"14\"\n          issue-comment: >\n            This thread has been automatically locked since there has not been\n            any recent activity after it was closed. If you are still experiencing a\n            similar issue, please open a new bug, including logs and a minimal\n            reproduction of the issue.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Flutter/Dart specific\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n*.g.dart\n*.freezed.dart\n*.mocks.dart\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.kotlin\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n**/android/app/.cxx/\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/**/GeneratedPluginRegistrant.*\n\n# macOS\n**/macos/Flutter/ephemeral\n**/macos/Flutter/GeneratedPluginRegistrant.swift\n**/macos/Flutter/Flutter-Debug.xcconfig\n**/macos/Flutter/Flutter-Release.xcconfig\n**/macos/Flutter/Flutter-Profile.xcconfig\n\n# Coverage\ncoverage/\n*.lcov\n\n# IDE\n.idea/\n.vscode/\n*.iml\n*.ipr\n*.iws\n.DS_Store\n\n# Localization\nlib/l10n/\n*.mo\n\n# Temporary files\n*.log\n*.tmp\n*.temp\n"
  },
  {
    "path": ".idx/dev.nix",
    "content": "{pkgs}: {\n  channel = \"stable-24.05\";\n  packages = [\n    pkgs.jdk17\n    pkgs.unzip\n  ];\n  idx.extensions = [\n    \n  ];\n  idx.previews = {\n    previews = {\n      web = {\n        command = [\n          \"flutter\"\n          \"run\"\n          \"--machine\"\n          \"-d\"\n          \"web-server\"\n          \"--web-hostname\"\n          \"0.0.0.0\"\n          \"--web-port\"\n          \"$PORT\"\n        ];\n        manager = \"flutter\";\n      };\n      android = {\n        command = [\n          \"flutter\"\n          \"run\"\n          \"--machine\"\n          \"-d\"\n          \"android\"\n          \"-d\"\n          \"localhost:5555\"\n        ];\n        manager = \"flutter\";\n      };\n    };\n  };\n}"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"b45fa18946ecc2d9b4009952c636ba7e2ffbb787\"\n  channel: \"stable\"\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n      base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n    - platform: android\n      create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n      base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n    - platform: ios\n      create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n      base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 更新日誌\n\n本專案的所有重要更改都將記錄在此文件中。\n\n格式基於 [如何維護更新日誌](https://keepachangelog.com/zh-TW/1.1.0/)，\n並且本專案遵循 [語意化版本](https://semver.org/lang/zh-TW/spec/v2.0.0.html)。\n\n## [未發布]\n\n### 新增\n\n### 更改\n\n### 修正\n\n## [3.1.402]\n\n### 新增\n- EEW 震度色塊\n- 風向圖卡\n- 捷徑功能\n\n### 更改\n- 介面樣式設定\n- 改善 App 啟動速度 (Android) (#490)\n- 地震報告取得更多資料\n- 更改首頁版面排序設定\n- 更新伺服器端點\n\n### 修正\n- 部分地點所在地預估震度問題\n- 地震報告詳細頁面返回問題\n\n## [3.1.401]\n\n### 新增\n- 地震重播\n\n### 更改\n- 支援平板介面\n- 部分網路用量降低\n\n### 修正\n- 地震報告框問題\n- 首頁底部點不到問題\n\n## [3.1.310]\n\n### 新增\n- 新增首頁樣式設定 (#478)\n\n### 更改\n- 重構天氣測站資訊 (#470)\n- 改善 App 啟動速度 (#475)\n\n### 修正\n- 手動定位 部分地點問題 (#474) (#477)\n- 自動定位異常掛掉問題 (#473) (Android)\n\n## [3.1.306]\n\n### 新增\n- 新增「事件切換」功能，可在全國／所在地，以及生效中／歷史之間切換查看 (#459)\n\n### 更改\n- 重構 GPS 定位邏輯以降低電量消耗，有助於提升使用者續航體驗 (#458) (Android)\n- 核心框架更新，提升穩定性\n\n### 修正\n- 修正歷史事件未定義，現在可以看到事件了(#459)\n- 天氣類圖層可以直接點選切換，圖層長按即可疊加顯示（例如雷達回波）\n- 大(豪)雨特報頁面左上角顯示時間問題 (#464)\n\n\n## [3.1.104] - 2025-11-11\n\n### 新增\n- 新增閃電圖層 (#435)\n- 新增強震監視器延遲顯示，可顯示資料延遲狀態 (#442)\n\n### 更改\n- 改善強震監視器頁面效能。\n- 重構 installId 初始化邏輯。 (#432)\n\n### 修正\n- 修正 P、S 波動畫在 FPS 設定下的順暢度問題。\n- 修正 EEW 資料未正確過濾 CWA 來源的問題。 (#428)\n- 修正字體網址問題。 (#434)\n- 修正關閉自動定位設定後，仍會異常執行自動定位的問題。(Android) (#432)\n\n## [3.1.103] - 2025-11-05\n\n### 新增\n\n- 新增通知設定提示。 (#409)\n- 新增自動縮放監視器地圖的設定開關。 (#426)\n\n### 更改\n\n- 核心框架更新。\n- 移除部分套件以減少封裝大小。\n- 更新依賴套件。\n\n### 修正\n\n- 修正部分頁面地點沒有區的問題。 (#402)\n- 修正背景自動定位沒有被執行的問題。 (#413) (#417)\n- 修正地震速報時 box 顯示問題。 (#415)\n- 修正大(豪)雨特報圖層順序問題。 (#418)\n- 修正檢知時 box 圖層順序問題。 (#421)\n\n## [3.1.001] - 2025-08-18\n\n### 修正\n\n- iOS手勢返回頁面問題。\n- 緊急地震資訊倒數異常。\n\n## [3.1.0] - 2025-08-14\n\n### 新增\n\n- 現在可以點擊通知跳轉地震報告頁面。\n\n### 更改\n\n- 一部分翻新測試通知本文(防災資訊)。\n- 更新翻譯。\n\n### 修正\n\n- 修正系統主題色變更問題。\n- 修正地震速報倒數顯示異常。\n- 修正歷史事件異常。\n- 修正重新載入取得最新資料。\n\n## [3.0.2] - 2025-08-13\n\n### 新增\n\n- 新增地圖圖例。\n- 地圖放大後現在會顯示每個觀測點的資料。\n- 手動設定所在地現在可以設定多個地點來快速切換。\n\n### 更改\n\n- 贊助頁面邏輯。\n\n### 修正\n\n- 首頁歷史事件時間線順序和時間軸連接線。\n- 強震監視器顯示時間數值位移的問題。\n- 雷達迴波被世界地圖圖層覆蓋的問題。\n\n## [3.0.014] - 2025-07-23\n\n### 新增\n\n- 新增初始圖層設定。\n- 強震監視器時間顯示。\n\n### 更改\n\n- 移除歷史頁面（雷雨、震度速報等）多餘的圖示包層。\n- 移除部分套件以減少封裝大小。\n\n### 修正\n\n- 部分 Android 裝置通知權限取得異常的問題。\n- Mac 上的 iOS 應用程式無法取得通知權限的問題。\n\n## [3.0.013] - 2025-07-14\n\n### 新增\n\n- 新增雷達回波播放功能。\n- 新增地圖動畫幀率設定。\n\n### 更改\n\n- 地區語系選擇改為使用圖示代替國旗。\n\n### 修正\n\n- 震度顯示文字錯誤。\n- 繁體中文語系選擇未顯示勾選狀態。\n- 部份測試通知無法運作。\n- 通知設定未正確同步的問題。\n- 事件頁面中 雷達圖層 顯示錯誤的問題。\n- 事件頁面中 GPS 圖層顯示錯誤的問題。\n- 雷達迴波圖層被底圖樣式遮蓋的問題。\n- 修正其他小問題。\n\n## [3.0.012] - 2025-07-09\n\n### 新增\n\n- 新增地圖基礎選擇器功能。\n- 新增強震監視器圖層。\n- 新增點擊強震監視器通知跳轉到頁面。\n\n### 更改\n\n- 改善 Apple 平台啟動效能，跳過不必要的 FCM 初始化流程。\n- 改善 town.json 座標資料。\n\n### 修正\n\n- 修正降雨圖層顯示與隱藏問題。\n- 修正 iOS 自動定位功能。\n- 修正切換自動定位開關的問題。\n- 修正圖片下載錯誤。\n- 修正雷雨圖卡導航。\n- 修正其他小問題。\n\n## [3.0.011] - 2025-05-31\n\n### 新增\n\n- 新地圖架構 (e28c1bb7dca509cb5878af77c61faabcc34016e2) (4cc4c8f477d19aeb72b1fe274e34be2c0c0bc8a6)\n- 新增地震報告 (cf66e3f)\n\n### 更改\n\n- 所在地頁面中隱藏所有 Android 的自啟動提示警告。\n- Apple 裝置啟動畫面 icon 改為螢幕寬度的 30%。 (75800b3)\n- 重構 Apple 裝置型號對應邏輯。 (f784217)\n\n## [3.0.010] - 2025-05-23\n\n### 更改\n\n- 地圖圖標改使用 sprite (ff3977692fa354d00912d99474e755d512e70421)\n\n### 修正\n\n- 修正即時天氣 API 資料格式變動 (40bc10030706a609e3aef499e5ed9bc61d3ef81d)\n- 修正 Android 12+ 裝置啟動畫面重複顯示的問題。\n\n## [3.0.009] - 2025-05-20\n\n### 更改\n\n- 國旗圖示改用內建圖片，App 更輕盈。\n\n### 修正\n\n- 修正 iOS 交易取消後無法再次發起購買的問題。\n\n## [3.0.008] - 2025-05-18\n\n### 新增\n\n- 新增 iOS 啟動畫面 icon。\n- 新增 Play 商店更新功能。 (d367c01f2f10395ad7d04e28c8b335d7779779d5)\n\n### 更改\n\n- 移除部分套件以減少封裝大小。\n- 移除 iOS 停用 icon 資源。\n\n### 修正\n\n- 修正 Android 自動定位服務 icon 問題。\n- 修正 iOS 交易完成後未正確結束導致無法再次購買的問題。\n- 修正震度文字顯示錯誤。 (7ff61e6f89be32d95ebe2769d03006e3e39b67de)\n\n## [3.0.007] - 2025-05-12\n\n### 新增\n\n- 合併推播通知設定和通知音效測試頁面。 (4fc8e4f88cfbbed7914020c7a0da347084fe034f)\n\n### 更改\n\n- 更改贊助免責聲明文字。\n\n### 修正\n\n- 修正首頁「雷雨即時訊息」卡片顯示邏輯。\n- 修正 iOS 裝置資訊顯示異常。\n- 修正通知測試文字描述。\n\n## [3.0.006] - 2025-05-08\n\n### 新增\n\n- 新增贊助免責聲明文字。\n- 重新設計更新日誌頁面。\n- 新增雷雨即時訊息卡片。\n- 新增首頁所在地未設定/服務區域外提示卡片。\n\n### 更改\n\n- 優化贊助頁面。\n\n### 修正\n\n- 首頁所在地按鈕位置未對齊其他按鈕。\n\n## [3.0.005] - 2025-05-06\n\n### 修正\n\n- 修正使用較大顯示器裝置時無法接受監控條款的問題。\n- 分開消耗性和非消耗性贊助項目。\n- 移除贊助價格中多餘的貨幣符號。\n- (iOS) 修正裝置資訊值溢出的問題。\n\n## [3.0.004] - 2025-05-05\n\n### 更改\n\n- 將贊助改為使用應用內購買。\n- 重構通知聲音測試頁面。\n\n## [3.0.003] - 2025-05-05\n\n## [3.0.002] - 2025-05-03\n\n## [3.0.001] - 2025-05-01\n\n[未發布]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.402...HEAD\n[3.1.402]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.401...v3.1.402\n[3.1.401]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.310...v3.1.401\n[3.1.310]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.3...v3.1.310\n[3.1.306]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.004...v3.1.306\n[3.1.104]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.003...v3.1.104\n[3.1.103]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.001...v3.1.103\n[3.1.001]: https://github.com/exptechtw/dpip-pocket/compare/v3.1.0...v3.1.001\n[3.1.0]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.2...v3.1.0\n[3.0.2]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.014...v3.0.2\n[3.0.014]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.013(2)...v3.0.014\n[3.0.013]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.012(1)...v3.0.013(2)\n[3.0.012]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.011...v3.0.012(1)\n[3.0.011]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.010...v3.0.011\n[3.0.010]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.009...v3.0.010\n[3.0.009]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.008...v3.0.009\n[3.0.008]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.007...v3.0.008\n[3.0.007]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.006...v3.0.007\n[3.0.006]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.005...v3.0.006\n[3.0.005]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.004...v3.0.005\n[3.0.004]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.003...v3.0.004\n[3.0.003]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.002...v3.0.003\n[3.0.002]: https://github.com/exptechtw/dpip-pocket/compare/v3.0.001...v3.0.002\n[3.0.001]: https://github.com/exptechtw/dpip-pocket/compare/2.5.500...v3.0.001\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project\n\nDPIP (Disaster Prevention Information Platform) — a Flutter app for Taiwan earthquake early warning and disaster information, integrating TREM-Net and CWA data.\n\n## Commands\n\n```bash\n# Install dependencies\nflutter pub get --no-example\n\n# Format\ndart format .\n\n# Lint\ndart analyze .\n\n# Code generation (required after editing routes, @JsonSerializable, or @freezed models)\ndart run build_runner build\n\n# Update translations\nbash tools/update_translations.sh\n\n# Run\nflutter run\n\n# Build\nflutter build apk --release\nflutter build ios --release\n```\n\nThere is no test suite.\n\n## Architecture\n\n**State management:** Provider (`ChangeNotifier`). Global providers are registered in `lib/core/providers.dart`:\n- `DpipDataModel` — earthquake/weather data\n- `SettingsLocationModel`, `SettingsMapModel`, `SettingsNotificationModel`, `SettingsUserInterfaceModel`\n\n**Routing:** go_router with type-safe routes via `@TypedGoRoute`. Routes are defined in `router.dart` and code-generated into `router.g.dart`. Run `build_runner` after route changes.\n\n**Feature modules** live under `lib/app/` (home, map, settings, changelog, debug, welcome). Each is self-contained with its own widgets subfolder.\n\n**API layer** (`lib/api/`): Dio HTTP client with caching. Models use `@JsonSerializable` + `@freezed` — regenerate with `build_runner` after changes.\n\n**Core services** (`lib/core/`): FCM, GPS, local notifications, EEW logic (`eew.dart`), compass, i18n, device info.\n\n**Assets:** JSON data files are gzip-compressed (`*.json.gz`) and decompressed at runtime. GLSL shaders (`fog.frag`, `thunderstorm.frag`) are used for map effects.\n\n## Key Conventions\n\n- **Settings naming:** Notification settings use numbered prefixes — `(1.eew)`, `(2.earthquake)`, `(3.weather)`, `(4.tsunami)`, `(5.basic)` — to control display order.\n- **Linting:** Extends `package:lint/strict.yaml`. Line width 100, preserve trailing commas, prefer single quotes.\n- **Documentation:** Every public member must have a doc comment (`///`). Every file must have a top-level library doc comment (`/// ...` before any `library` or first declaration). Follow Effective Dart: document usage and behavior from the caller's perspective, not internal implementation. Use `[...]` for code references, avoid restating the name.\n- **Dot shorthand:** Always use Dart dot shorthand (e.g., `.value` instead of `EnumType.value`) wherever the type can be inferred from context.\n- **Widget extraction:** When a build method nests too deeply, extract the subtree into a private widget class (`_FooWidget`) in the same file rather than keeping it inline.\n- **Class member order** (top to bottom), within each group sort alphabetically `A→Z` then `a→z`:\n  1. Class fields\n  2. Primary constructor\n  3. Named constructors\n  4. Uninitialized variables / private fields\n  5. Private methods\n  6. Public methods\n  7. Overriding methods — widgets follow lifecycle order: `initState` → `build` → `dispose`\n  8. Static fields\n  9. Static members\n- **Extensions:** Always prefer extension methods from `lib/utils/extensions/` over verbose equivalents. Avoid `.of(context)` calls — use `BuildContext` extensions instead:\n  - `context.theme` → `Theme.of(context)`\n  - `context.colors` → `Theme.of(context).colorScheme`\n  - `context.texts` → `Theme.of(context).textTheme`\n  - `context.dimension` → `MediaQuery.sizeOf(context)`\n  - `context.padding` → `MediaQuery.paddingOf(context)`\n  - `context.brightness` → `MediaQuery.platformBrightnessOf(context)`\n  - `context.navigator` → `Navigator.of(context)`\n  - `context.scaffoldMessenger` → `ScaffoldMessenger.of(context)`\n  - `context.router` → `GoRouter.of(context)`\n  - `context.popUntil(path)` → `GoRouter.of(context).popUntil(path)`\n  - `context.bottomSheetConstraints` → Material 3 bottom sheet constraints\n- **Generated files** (`**.freezed.dart`, `**.g.dart`) are excluded from analysis — do not edit them manually.\n- **Localization:** Uses `i18n_extension`. Translations are managed via Crowdin; only zh-Hant is updated locally via the translation script.\n- **Maps:** MapLibre GL with multiple layer managers. Dynamic color (Material You) via `dynamic_color`.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# 貢獻者公約\n\n## 我們的承諾\n\n為了促進一個開放透明且受歡迎的環境，我們作為貢獻者和維護者保證，無論年齡、種族、民族、性別認同和表達、體型、殘疾、經驗水平、國籍、個人表現、宗教或性別取向，在我們的專案以及社群的參與者都有不被騷擾的體驗。\n\n## 我們的準則\n\n舉例來說有助於創造正面環境的行為包括：\n\n* 使用歡迎和包容性語言\n* 尊重不同的觀點和經驗\n* 優雅地接受建設性批評\n* 關注在對於社群最好的事情上\n* 對其他社群成員的表現友善\n\n舉例來說身為參與者不能接受的行為包括：\n\n* 使用與性有關的言語或是圖像，以及不受歡迎的性騷擾\n* 酸民/反串/釣魚行為或進行侮辱/貶損的評論，人身攻擊及政治攻擊\n* 公開或私下的騷擾\n* 未經許可地發布他人的個人資料，例如住址或是電子地址\n* 其他可以被合理地認定為不恰當或者違反職業操守的行為\n\n## 我們的責任\n\n專案維護者有責任為「可接受的行為」準則做出詮釋，以及對已發生的不被接受的行為採取恰當且公平的糾正措施。\n\n專案維護者有權力及責任去刪除、編輯、拒絕與本行為準則有所違背的評論（comments）、提交（commits）、程式碼、wiki 編輯、問題（issues）和其他貢獻，以及專案維護者可暫時或永久性的禁止任何他們認為有不適當、威脅、冒犯、有害行為的貢獻者。\n\n## 使用範圍\n\n當一個人代表該專案或是其社群時，本行為準則適用於其專案平台和公共平台。\n\n代表專案或是社群的情況，舉例來說包括使用官方專案的電子郵件地址、通過官方的社群媒體帳號發布或線上或線下事件中擔任指定代表。\n\n該專案的呈現方式可由其專案維護者進行進一步的定義及解釋。\n\n## 強制執行\n\n可以透過 [support@exptech.dev](mailto:support@exptech.dev) 聯繫專案團隊來報告濫用、騷擾或其他不被接受的行為。\n\n任何維護團隊認為有必要且適合的所有投訴都將進行審查及調查，並做出相對應的回應。專案小組有對事件回報者有保密的義務。具體執行的方針近一步細節可能會單獨公佈。\n\n沒有真誠的遵守或是執行本行為準則的專案維護人員，可能會因專案領導人或是其他成員的決定，暫時或是永久的取消其身份。\n\n## 來源\n\n本行為準則改編自[貢獻者公約][首頁]，版本 1.4\n可在此觀看 https://www.contributor-covenant.org/zh-tw/version/1/4/code-of-conduct.html\n\n[首頁]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "LICENSE",
    "content": "DPIP Public License\nCopyright (C) 2026 ExpTech Studio. All rights reserved.\n\n                        TERMS AND CONDITIONS\n\n0. DEFINITIONS\n\n   \"The Software\" refers to DPIP - Disaster Prevention Information Platform\n   and all associated source code, assets, and documentation.\n\n   \"You\" refers to any individual or organization exercising rights under\n   this License.\n\n   \"Distribute\" means to make the Software, or any modified version of it,\n   available to any third party by any means, including but not limited to\n   publishing, uploading, or shipping.\n\n   \"Modified Version\" means any work based on or derived from the Software\n   in which one or more portions have been changed, added, or removed.\n\n   \"Source Code\" means the preferred form of the Software for making\n   modifications to it, including all associated build scripts, assets,\n   and configuration files required to produce and run the Software.\n\n   \"Commercial Use\" means any use of the Software or a Modified Version\n   primarily intended for or directed toward commercial advantage or\n   monetary compensation.\n\n1. GRANT OF RIGHTS\n\n   Subject to the terms of this License, you are granted a worldwide,\n   royalty-free, non-sublicensable right to:\n\n   a) View and study the Source Code.\n   b) Run the Software for personal, educational, and non-commercial purposes.\n   c) Modify the Software for personal, educational, and non-commercial purposes.\n   d) Distribute verbatim or Modified Versions of the Software, subject to\n      the conditions in Section 2.\n\n2. CONDITIONS FOR DISTRIBUTION\n\n   If you Distribute the Software or any Modified Version, you must:\n\n   a) INCLUDE SOURCE CODE. Provide the complete corresponding Source Code,\n      either alongside the distribution or via a publicly accessible location.\n      You may not distribute compiled or binary forms without the Source Code.\n\n   b) PRESERVE NOTICES. Retain all copyright notices, license notices, and\n      attributions present in the original Software. If the Software displays\n      copyright or license notices to users, Modified Versions must do the same.\n\n   c) MARK MODIFICATIONS. Clearly state that you have modified the Software\n      and include a description of the changes made and the date of modification.\n\n   d) USE THIS LICENSE. The entire distribution, including all Modified Versions,\n      must be licensed under this License. You may not apply any additional\n      restrictions or grant any additional permissions beyond those in this License.\n\n   e) NO SUBLICENSING. You may not sublicense the Software. Each recipient\n      receives their rights directly under this License.\n\n3. NO COMPETING USE\n\n   You may not use the Software, or any Modified Version of it, to offer\n   a product or service that competes, directly or indirectly, with the\n   Software or with any product or service offered by ExpTech Studio.\n\n   This includes, but is not limited to:\n\n   a) Rebranding the Software and offering it as your own product.\n   b) Using the Software as the foundation of a competing disaster\n      prevention information platform or alert service.\n   c) Offering a hosted or managed version of the Software to third\n      parties as a standalone product.\n\n4. NO COMMERCIAL USE\n\n   You may not use the Software, or any Modified Version of it, for Commercial\n   Use. This includes, but is not limited to:\n\n   a) Selling the Software or Modified Versions, in whole or in part.\n   b) Offering the Software or Modified Versions as a paid product or service.\n   c) Incorporating the Software or Modified Versions into a commercial product.\n   d) Using the Software or Modified Versions to provide a service for which\n      you charge a fee.\n\n   If you wish to use the Software for commercial purposes, you must obtain\n   a separate written license from ExpTech Studio.\n\n5. PATENT RIGHTS\n\n   Each contributor grants you a non-exclusive, worldwide, royalty-free license\n   under their essential patent claims to make, use, and distribute the Software\n   in accordance with this License. If you initiate patent litigation against\n   any party claiming that the Software infringes a patent, your rights under\n   this License terminate immediately.\n\n6. TERMINATION\n\n   Your rights under this License terminate automatically if you violate any of\n   its terms. Upon termination, you must cease all distribution of the Software\n   and any Modified Versions.\n\n   If you cure the violation within 30 days of becoming aware of it, your rights\n   may be reinstated permanently, provided this is the first time you have\n   violated this License.\n\n7. NO WARRANTY\n\n   THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n   FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. EXPTECH STUDIO DOES\n   NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR THAT ANY ERRORS WILL BE\n   CORRECTED.\n\n8. LIMITATION OF LIABILITY\n\n   IN NO EVENT SHALL EXPTECH STUDIO OR ANY CONTRIBUTOR BE LIABLE FOR ANY DIRECT,\n   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING\n   OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE, EVEN\n   IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n                        END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "[![splash](/.github/assets/splash.png)](#下載)\n\n<div align=\"center\">\n<a href=\"https://github.com/ExpTechTW/DPIP-Pocket/tree/main\"><img alt=\"status\" src=\"https://img.shields.io/badge/status-stable-blue.svg\"></a>\n<a href=\"https://github.com/ExpTechTW/DPIP-Pocket/releases/latest\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/release/exptechtw/dpip-pocket\"></a>\n<a href=\"https://github.com/ExpTechTW/DPIP-Pocket/actions/workflows/android.yml\"><img alt=\"Android Build Status\" src=\"https://github.com/ExpTechTW/DPIP-Pocket/actions/workflows/android.yml/badge.svg\"></a>\n<a href=\"https://github.com/ExpTechTW/DPIP-Pocket/actions/workflows/ios.yml\"><img alt=\"iOS Build Status\" src=\"https://github.com/ExpTechTW/DPIP-Pocket/actions/workflows/ios.yml/badge.svg\"></a>\n<a title=\"Crowdin\" target=\"_blank\" href=\"https://crowdin.com/project/dpip\"><img alt=\"Crowdin Localization\" src=\"https://badges.crowdin.net/dpip/localized.svg\"></a>\n<a href=\"https://good-labs.github.io/greater-good-affirmation\"><img alt=\"Greater Good\" src=\"https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg\"></a>\n<img alt=\"GitHub License\" src=\"https://img.shields.io/github/license/exptechtw/dpip-pocket\">\n<a href=\"https://exptech.dev\"><img alt=\"website\" src=\"https://img.shields.io/badge/website-exptech.dev-purple.svg\"></a>\n<a href=\"https://discord.gg/5dbHqV8ees\"><img alt=\"TREM Discord\"  src=\"https://img.shields.io/discord/926545182407688273?color=%235865F2&logo=discord&logoColor=white\"></a>\n</div>\n\nDPIP（Disaster Prevention Information Platform）是由臺灣本土團隊開發的行動應用程式，整合了 TREM-Net（臺灣即時地震觀測網）的強震即時警報與地震資訊，以及中央氣象署的資料，為使用者提供一個整合、便捷的防災資訊平台。\n\n### 強震即時警報\n\n強震即時警報（Earthquake Early Warning, EEW）系統透過部署於各地的地震波觀測站，在地震發生時即時回傳地震波數據至伺服器進行分析，並產生地震速報。這項技術能為使用者爭取數秒至數十秒的寶貴時間，讓民眾能及時採取防災應變及避難措施。\n\n### TREM-Net 臺灣即時地震觀測網\n\nTREM-Net 是一個自 2022 年 6 月起開始在全臺各地部署的觀測網專案，由兩個子系統組成：**SE-Net**（強震觀測網，使用加速度儀）及 **MS-Net**（微震觀測網，使用速度儀），共同記錄地震發生時的完整數據。\n\n## 合作夥伴\n\n我們很榮幸能與以下優秀的企業合作，共同推動防災資訊的普及：\n\n<h3>\n  <a href=\"https://www.geoscience.com.tw/\">\n    巨科資訊有限公司\n    <img src=\"https://github.com/user-attachments/assets/34875ff1-ace2-4e92-ac32-d98e5717b62e\" alt=\"巨科資訊有限公司\" width=\"auto\" height=\"28\" align=\"right\">\n  </a>\n</h3>\n\n巨科資訊有限公司是一家專注於地理資訊系統的專業公司，為我們的開發工作提供了寶貴的設備支援。他們的專業知識和資源對專案的發展起到了重要作用。\n\n<h3>\n  <a href=\"https://www.twds.com.tw/\">\n    台灣數位串流有限公司\n    <img src=\"https://branding.twds.com.tw/assets/twds_text_standard.svg\" alt=\"台灣數位串流有限公司\" width=\"auto\" height=\"28\" align=\"right\">\n  </a>\n</h3>\n\n台灣數位串流有限公司為我們提供了強大的雲端運算資源和網路頻寬支援，同時也提供了寶貴的技術諮詢。他們的支援確保了我們的服務能夠穩定且高效地運行。\n\n我們由衷感謝這些合作夥伴對開源社群的支持與貢獻。正是有了他們的協助，我們才能持續為使用者提供更好的服務。\n\n## 資料來源\n\n本應用程式的資料來源包括：\n\n### 官方來源\n\n- [交通部中央氣象署](https://www.cwa.gov.tw/)\n- [國家災害防救科技中心](https://www.ncdr.nat.gov.tw/)\n\n### 非官方來源\n\n- TREM-Net by [ExpTech Studio](https://exptech.dev/)\n\n## 下載\n\n你可以在 [Play Store](https://play.google.com/store/apps/details?id=com.exptech.dpip) 和 [App Store](https://apps.apple.com/tw/app/dpip-%E7%81%BD%E5%AE%B3%E5%A4%A9%E6%B0%A3%E8%88%87%E5%9C%B0%E9%9C%87%E9%80%9F%E5%A0%B1/id6468026362) 上取得 DPIP。\n\n你也可以從我們的 [Release 頁面](https://github.com/ExpTechTW/DPIP-Pocket/releases/latest)上取得 DPIP 的安裝包進行手動安裝。\n\n## 翻譯\n\nDPIP 支援多語言，我們正在 Crowdin 平台上進行翻譯。如果你願意協助我們將這個專案翻譯成其他語言，歡迎加入我們的 Crowdin 翻譯社群。\n\n你可以[點擊這裡前往我們的 Crowdin 專案頁面](https://crowdin.com/project/dpip)，選擇你熟悉的語言並開始翻譯。每一份貢獻都將幫助我們將防災資訊傳遞給更多的人！\n\n如果你沒有看到你熟悉的語言，歡迎在我們的 [Issue](https://github.com/ExpTechTW/DPIP-Pocket/issues) 中提出新的語言請求，我們會盡快為你開啟。\n\n## 從原始碼建置\n\n### 環境需求\n\n在開始建置之前，請確保你的開發環境已安裝並配置以下軟體：\n\n- **Flutter SDK**: [安裝指引](https://docs.flutter.dev/get-started/install)\n- **Dart SDK**: 已包含在 Flutter SDK 中\n- [**Android Studio**](https://developer.android.com/studio?hl=ja) 或 [**Xcode**](https://developer.apple.com/jp/xcode/)（iOS 開發用）\n  - 也可以使用 [VSCode](https://code.visualstudio.com/) 或其他你喜歡的 IDE\n- _\\*可選\\*_ [**Git**](https://git-scm.com/): 用於複製存儲庫\n\n```console\nFlutter 3.35.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 20f8274939 • 2025-08-14 10:53:09 -0700\nEngine • hash 6cd51c08a88e7bbe848a762c20ad3ecb8b063c0e • 2025-08-13 23:35:25.000Z\nTools • Dart 3.9.0 • DevTools 2.48.0\n```\n\n### 建置步驟\n\n1. 取得原始碼\n\n   - **下載壓縮檔**\n\n     你可以直接在 Github 上下載存儲庫壓縮檔\n\n     ![Download Source ZIP](/.github/assets/download_source.png)\n\n   - **使用 Git**\n\n     使用以下指令複製專案：\n\n     ```bash\n     git clone https://github.com/ExpTechTW/DPIP-Pocket.git\n     ```\n\n2. 進入專案目錄\n\n   ```bash\n   cd DPIP\n   ```\n\n3. 安裝相依套件\n\n   ```bash\n   flutter pub get --no-example\n   ```\n\n4. 產生建置檔案\n\n   ```bash\n   dart run build_runner build\n   ```\n\n5. 建置應用程式\n\n   - **Android APK**\n\n     ```bash\n     flutter build apk --release\n     ```\n\n   - **iOS**\n\n     ```bash\n     flutter build ios --release\n     ```\n\n## 如何貢獻\n\n我們歡迎各種形式的貢獻！你可以透過以下方式參與專案：\n\n- 回報問題或提出新功能建議：請在 [Issues](https://github.com/ExpTechTW/DPIP-Pocket/issues) 中提出\n- 提交程式碼：請 [Fork](https://github.com/ExpTechTW/DPIP-Pocket/fork) 此倉庫，建立新分支進行修改，然後提交 [Pull Request](https://github.com/ExpTechTW/TREM/pulls)\n- 改進文件：協助我們改進現有文件或撰寫新文件\n\n衷心感謝所有讓 DPIP 成為可能的貢獻者：\n\n<a href=\"https://github.com/exptechtw/DPIP-Pocket/graphs/contributors\"><img src=\"https://contrib.rocks/image?repo=exptechtw/DPIP-Pocket\" ></a>\n\n## 開放原始碼授權\n\n詳細的授權資訊請參閱 [LICENSE](LICENSE) 檔案\n\n## Star History\n\n<a href=\"https://star-history.com/#ExpTechTW/DPIP-Pocket&Date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=ExpTechTW/DPIP-Pocket&type=Date&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=ExpTechTW/DPIP-Pocket&type=Date\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=ExpTechTW/DPIP-Pocket&type=Date\" />\n </picture>\n</a>\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "include: package:lint/strict.yaml\n\nanalyzer:\n  errors:\n    todo: info\n  exclude:\n    - \"**.freezed.dart\"\n    - \"**.g.dart\"\n\nlinter:\n  rules:\n    flutter_style_todos: error\n    prefer_single_quotes: warning\n    avoid_annotating_with_dynamic: error\n    public_member_api_docs: warning\n\nformatter:\n  page_width: 100\n  trailing_commas: preserve\n"
  },
  {
    "path": "android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "plugins {\n    id \"com.android.application\"\n    id \"org.jetbrains.kotlin.android\"\n    id \"dev.flutter.flutter-gradle-plugin\"\n    id \"com.google.gms.google-services\"\n    id \"com.google.firebase.crashlytics\"\n}\n\ndef localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0.0'\n}\n\ndef versionParts = flutterVersionName.split(\"\\\\.\")\ndef majorVersion = versionParts[0].padLeft(2, '0')\ndef minorVersion = versionParts[1].padLeft(2, '0')\ndef patchVersion = versionParts[2].padLeft(2, '0')\n\ndef flutterVersionCode = (majorVersion + minorVersion + patchVersion).toInteger()\n\nandroid {\n    namespace 'com.exptech.dpip'\n    compileSdk flutter.compileSdkVersion\n    ndkVersion '28.2.13676358'\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n        coreLibraryDesugaringEnabled true\n    }\n\n    kotlinOptions {\n        jvmTarget = '17'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        applicationId 'com.exptech.dpip'\n        minSdkVersion 26\n        targetSdkVersion 36\n        versionCode 300201001\n        versionName flutterVersionName\n        multiDexEnabled true\n        resConfigs \"en\", \"ko\", \"zh-rTW\", \"ja\", \"zh-rCN\"\n    }\n\n    signingConfigs {\n        release {\n            def keystorePropsFile = rootProject.file('key.properties')\n            if (keystorePropsFile.exists()) {\n                def keystoreProps = new Properties()\n                keystoreProps.load(new FileInputStream(keystorePropsFile))\n\n                keyAlias keystoreProps['keyAlias']\n                keyPassword keystoreProps['keyPassword']\n                storeFile file(keystoreProps['storeFile'])\n                storePassword keystoreProps['storePassword']\n            } else {\n                println('key.properties not found')\n            }\n        }\n    }\n\n    buildTypes {\n        release {\n            signingConfig signingConfigs.release\n            minifyEnabled true\n            shrinkResources false\n            multiDexEnabled true\n        }\n        debug {\n            signingConfig signingConfigs.debug\n            minifyEnabled false\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation 'androidx.core:core-splashscreen:1.2.0'\n    implementation 'com.google.android.material:material:1.13.0'\n    implementation 'com.google.android.gms:play-services-location:21.3.0'\n    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'\n}\n"
  },
  {
    "path": "android/app/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"141632948166\",\n    \"project_id\": \"dpip-a658c\",\n    \"storage_bucket\": \"dpip-a658c.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:141632948166:android:3cb29f28a0a04c589e14c7\",\n        \"android_client_info\": {\n          \"package_name\": \"com.exptech.dpip\"\n        }\n      },\n      \"oauth_client\": [],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyDMgbvC0NWLL2CfRChypf3yy7OWxEO-I0w\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": []\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_LOCATION\" />\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_NOTIFICATION_POLICY\"/>\n\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.SCHEDULE_EXACT_ALARM\"/>\n    <uses-permission android:name=\"android.permission.USE_EXACT_ALARM\"/>\n\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\"/>\n\n    <uses-permission android:name=\"android.permission.VIBRATE\"/>\n\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" android:maxSdkVersion=\"28\"/>\n\n    <uses-sdk tools:overrideLibrary=\"com.ytn.autostarter\" />\n\n    <application\n            android:label=\"@string/app_name\"\n            android:name=\"${applicationName}\"\n            android:icon=\"@mipmap/ic_launcher\"\n            android:theme=\"@style/LaunchTheme\"\n            android:enableOnBackInvokedCallback=\"true\"\n            android:allowBackup=\"false\"\n            android:fullBackupContent=\"false\"\n            tools:replace=\"android:allowBackup,android:fullBackupContent\">\n        <activity\n                android:name=\".MainActivity\"\n                android:exported=\"true\"\n                android:largeHeap=\"true\"\n                android:launchMode=\"singleTop\"\n                android:theme=\"@style/NormalTheme\"\n                android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n                android:hardwareAccelerated=\"true\"\n                android:windowSoftInputMode=\"adjustResize\"\n                android:showWhenLocked=\"true\"\n                android:turnScreenOn=\"true\">\n            <meta-data\n                    android:name=\"io.flutter.embedding.android.NormalTheme\"\n                    android:resource=\"@style/NormalTheme\"/>\n            <meta-data\n                    android:name=\"android.app.shortcuts\"\n                    android:resource=\"@xml/shortcuts\" />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <meta-data\n                android:name=\"flutterEmbedding\"\n                android:value=\"2\"/>\n        <meta-data android:name=\"com.google.firebase.messaging.default_notification_icon\" android:resource=\"@drawable/ic_stat_name\"/>\n        <meta-data android:name=\"com.exptech.dpip.default_notification_icon\" android:resource=\"@drawable/ic_stat_name\"/>\n\n        <service\n                android:name=\".LocationForegroundService\"\n                android:enabled=\"true\"\n                android:exported=\"false\"\n                android:foregroundServiceType=\"location\"/>\n        <!-- AlarmManager service -->\n        <service\n                android:name=\"dev.fluttercommunity.plus.androidalarmmanager.AlarmService\"\n                android:permission=\"android.permission.BIND_JOB_SERVICE\"\n                android:exported=\"false\"/>\n        <receiver\n                android:name=\"dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver\"\n                android:exported=\"false\"/>\n        <receiver\n                android:name=\"dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver\"\n                android:enabled=\"true\"\n                android:exported=\"false\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\"/>\n            </intent-filter>\n        </receiver>\n    </application>\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\"/>\n            <data android:mimeType=\"text/plain\"/>\n        </intent>\n    </queries>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt",
    "content": "package com.exptech.dpip\n\nimport android.content.Intent\nimport android.os.Bundle\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.MethodChannel\n\nclass MainActivity : FlutterActivity() {\n\n    private val CHANNEL = \"com.exptech.dpip/shortcut\"\n    private var initialShortcut: String? = null\n\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)\n            .setMethodCallHandler { call, result ->\n                when (call.method) {\n                    \"getInitialShortcut\" -> {\n                        result.success(initialShortcut)\n                        initialShortcut = null\n                    }\n                    else -> result.notImplemented()\n                }\n            }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        handleIntent(intent)\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        setIntent(intent)\n        handleIntent(intent)\n    }\n\n    private fun handleIntent(intent: Intent?) {\n        intent ?: return\n        val shortcutId = intent.getStringExtra(\"android.intent.extra.shortcut.ID\")\n            ?: intent.getStringExtra(\"shortcut\")\n        if (!shortcutId.isNullOrEmpty()) {\n            initialShortcut = shortcutId\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@mipmap/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@mipmap/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/colors.xml",
    "content": "<resources>\n    <color name=\"splash_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DPIP</string>\n    <string name=\"shortcut_monitor_short\">Monitor</string>\n    <string name=\"shortcut_monitor_long\">Open Earthquake Monitor</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-ja/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DPIP防災</string>\n    <string name=\"shortcut_monitor_short\">強震モニター</string>\n    <string name=\"shortcut_monitor_long\">強震モニターを開く</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values-ko/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DPIP 재해</string>\n    <string name=\"shortcut_monitor_short\">강진 모니터</string>\n    <string name=\"shortcut_monitor_long\">강진 모니터 열기</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values-night/colors.xml",
    "content": "<resources>\n    <color name=\"splash_background\">#000000</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night-v31/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"Theme.SplashScreen\">\n        <item name=\"android:windowSplashScreenBackground\">@color/splash_background</item>\n        <item name=\"android:windowSplashScreenAnimatedIcon\">@mipmap/ic_launcher_round</item>\n        <item name=\"android:windowSplashScreenIconBackgroundColor\">@color/splash_background</item>\n    </style>\n    <style name=\"NormalTheme\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-sw600dp/styles.xml",
    "content": "<resources>\n    <style name=\"NormalTheme\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n        <item name=\"android:screenOrientation\">unspecified</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-v31/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"Theme.SplashScreen\">\n        <item name=\"android:windowSplashScreenBackground\">@color/splash_background</item>\n        <item name=\"android:windowSplashScreenAnimatedIcon\">@mipmap/ic_launcher_round</item>\n        <item name=\"android:windowSplashScreenIconBackgroundColor\">@color/splash_background</item>\n    </style>\n    <style name=\"NormalTheme\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-zh/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DPIP 防災</string>\n    <string name=\"shortcut_monitor_short\">強震監視器</string>\n    <string name=\"shortcut_monitor_long\">打開強震監視器</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DPIP 防災</string>\n    <string name=\"shortcut_monitor_short\">強震監視器</string>\n    <string name=\"shortcut_monitor_long\">打開強震監視器</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/xml/shortcuts.xml",
    "content": "<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n            android:shortcutId=\"monitor\"\n            android:enabled=\"true\"\n            android:icon=\"@mipmap/ic_launcher\"\n            android:shortcutShortLabel=\"@string/shortcut_monitor_short\"\n            android:shortcutLongLabel=\"@string/shortcut_monitor_long\">\n        <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:targetPackage=\"com.exptech.dpip\"\n                android:targetClass=\"com.exptech.dpip.MainActivity\" >\n        <extra android:name=\"shortcut\" android:value=\"monitor\" />\n        </intent>\n    </shortcut>\n</shortcuts>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          xmlns:tools=\"http://schemas.android.com/tools\">\n    android:installLocation=\"internalOnly\"\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>\n    <uses-permission android:name=\"android.permission.VIBRATE\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\"/>\n    <uses-permission android:minSdkVersion=\"34\" android:name=\"android.permission.USE_EXACT_ALARM\" />\n\n    <application\n            tools:replace=\"android:label\"\n            android:label=\"DPIP\"\n            android:name=\"${applicationName}\"\n            android:icon=\"@mipmap/ic_launcher\">\n        <activity\n                android:name=\".MainActivity\"\n                android:exported=\"true\"\n                android:launchMode=\"singleTop\"\n                android:theme=\"@style/LaunchTheme\"\n                android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n                android:hardwareAccelerated=\"true\"\n                android:windowSoftInputMode=\"adjustResize\"\n                android:showWhenLocked=\"true\"\n                android:turnScreenOn=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <meta-data\n                android:name=\"flutterEmbedding\"\n                android:value=\"2\"/>\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\n\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.2.0-bin.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nkotlin.jvm.target.validation.mode = IGNORE\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "pluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\").withInputStream { properties.load(it) }\n        def flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n        assert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\n        return flutterSdkPath\n    }\n    settings.ext.flutterSdkPath = flutterSdkPath()\n\n    includeBuild(\"${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id \"dev.flutter.flutter-plugin-loader\" version \"1.0.0\"\n    id 'com.android.application' version '8.13.2' apply false\n    id 'org.jetbrains.kotlin.android' version '2.2.10' apply false\n    id \"com.google.gms.google-services\" version \"4.4.2\" apply false\n    id \"com.google.firebase.crashlytics\" version \"3.0.2\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "assets/box.json",
    "content": "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,25.1],[121.38,25.46],[120.89,25.46],[120.89,25.1],[121.38,25.1]]]},\"properties\":{\"ID\":0}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,25.1],[121.86,25.46],[121.38,25.46],[121.38,25.1],[121.86,25.1]]]},\"properties\":{\"ID\":1}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[122.35,25.1],[122.35,25.46],[121.86,25.46],[121.86,25.1],[122.35,25.1]]]},\"properties\":{\"ID\":2}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,24.74],[121.38,25.1],[120.89,25.1],[120.89,24.74],[121.38,24.74]]]},\"properties\":{\"ID\":3}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,24.74],[121.86,25.1],[121.38,25.1],[121.38,24.74],[121.86,24.74]]]},\"properties\":{\"ID\":4}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[122.35,24.74],[122.35,25.1],[121.86,25.1],[121.86,24.74],[122.35,24.74]]]},\"properties\":{\"ID\":5}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,24.38],[120.89,24.74],[120.4,24.74],[120.4,24.38],[120.89,24.38]]]},\"properties\":{\"ID\":6}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,24.38],[121.38,24.74],[120.89,24.74],[120.89,24.38],[121.38,24.38]]]},\"properties\":{\"ID\":7}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,24.38],[121.86,24.74],[121.38,24.74],[121.38,24.38],[121.86,24.38]]]},\"properties\":{\"ID\":8}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[122.35,24.38],[122.35,24.74],[121.86,24.74],[121.86,24.38],[122.35,24.38]]]},\"properties\":{\"ID\":9}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,24.02],[120.4,24.38],[119.92,24.38],[119.92,24.02],[120.4,24.02]]]},\"properties\":{\"ID\":10}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,24.02],[120.89,24.38],[120.4,24.38],[120.4,24.02],[120.89,24.02]]]},\"properties\":{\"ID\":11}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,24.02],[121.38,24.38],[120.89,24.38],[120.89,24.02],[121.38,24.02]]]},\"properties\":{\"ID\":12}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,24.02],[121.86,24.38],[121.38,24.38],[121.38,24.02],[121.86,24.02]]]},\"properties\":{\"ID\":13}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[119.92,23.66],[119.92,24.02],[119.43,24.02],[119.43,23.66],[119.92,23.66]]]},\"properties\":{\"ID\":14}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,23.66],[120.4,24.02],[119.92,24.02],[119.92,23.66],[120.4,23.66]]]},\"properties\":{\"ID\":15}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,23.66],[120.89,24.02],[120.4,24.02],[120.4,23.66],[120.89,23.66]]]},\"properties\":{\"ID\":16}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,23.66],[121.38,24.02],[120.89,24.02],[120.89,23.66],[121.38,23.66]]]},\"properties\":{\"ID\":17}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,23.66],[121.86,24.02],[121.38,24.02],[121.38,23.66],[121.86,23.66]]]},\"properties\":{\"ID\":18}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[119.43,23.3],[119.43,23.66],[118.95,23.66],[118.95,23.3],[119.43,23.3]]]},\"properties\":{\"ID\":19}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[119.92,23.3],[119.92,23.66],[119.43,23.66],[119.43,23.3],[119.92,23.3]]]},\"properties\":{\"ID\":20}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,23.3],[120.4,23.66],[119.92,23.66],[119.92,23.3],[120.4,23.3]]]},\"properties\":{\"ID\":21}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,23.3],[120.89,23.66],[120.4,23.66],[120.4,23.3],[120.89,23.3]]]},\"properties\":{\"ID\":22}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,23.3],[121.38,23.66],[120.89,23.66],[120.89,23.3],[121.38,23.3]]]},\"properties\":{\"ID\":23}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,23.3],[121.86,23.66],[121.38,23.66],[121.38,23.3],[121.86,23.3]]]},\"properties\":{\"ID\":24}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[119.43,22.94],[119.43,23.3],[118.95,23.3],[118.95,22.94],[119.43,22.94]]]},\"properties\":{\"ID\":25}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[119.92,22.94],[119.92,23.3],[119.43,23.3],[119.43,22.94],[119.92,22.94]]]},\"properties\":{\"ID\":26}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,22.94],[120.4,23.3],[119.92,23.3],[119.92,22.94],[120.4,22.94]]]},\"properties\":{\"ID\":27}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,22.94],[120.89,23.3],[120.4,23.3],[120.4,22.94],[120.89,22.94]]]},\"properties\":{\"ID\":28}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,22.94],[121.38,23.3],[120.89,23.3],[120.89,22.94],[121.38,22.94]]]},\"properties\":{\"ID\":29}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,22.94],[121.86,23.3],[121.38,23.3],[121.38,22.94],[121.86,22.94]]]},\"properties\":{\"ID\":30}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,22.58],[120.4,22.94],[119.92,22.94],[119.92,22.58],[120.4,22.58]]]},\"properties\":{\"ID\":31}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,22.58],[120.89,22.94],[120.4,22.94],[120.4,22.58],[120.89,22.58]]]},\"properties\":{\"ID\":32}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,22.58],[121.38,22.94],[120.89,22.94],[120.89,22.58],[121.38,22.58]]]},\"properties\":{\"ID\":33}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,22.58],[121.86,22.94],[121.38,22.94],[121.38,22.58],[121.86,22.58]]]},\"properties\":{\"ID\":34}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.4,22.22],[120.4,22.58],[119.92,22.58],[119.92,22.22],[120.4,22.22]]]},\"properties\":{\"ID\":35}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,22.22],[120.89,22.58],[120.4,22.58],[120.4,22.22],[120.89,22.22]]]},\"properties\":{\"ID\":36}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,22.22],[121.38,22.58],[120.89,22.58],[120.89,22.22],[121.38,22.22]]]},\"properties\":{\"ID\":37}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[120.89,21.86],[120.89,22.22],[120.4,22.22],[120.4,21.86],[120.89,21.86]]]},\"properties\":{\"ID\":38}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.38,21.86],[121.38,22.22],[120.89,22.22],[120.89,21.86],[121.38,21.86]]]},\"properties\":{\"ID\":39}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[121.86,21.86],[121.86,22.22],[121.38,22.22],[121.38,21.86],[121.86,21.86]]]},\"properties\":{\"ID\":40}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[118.46,24.38],[118.46,24.74],[117.97,24.74],[117.97,24.38],[118.46,24.38]]]},\"properties\":{\"ID\":41}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[118.95,24.38],[118.95,24.74],[118.46,24.74],[118.46,24.38],[118.95,24.38]]]},\"properties\":{\"ID\":42}}]}"
  },
  {
    "path": "assets/notify_test.json",
    "content": "{\"eew_alert-important-v2\":{\"title\":\"🚨 《緊急地震速報 (氣象署發布) 》\",\"body\":\"花蓮縣壽豐鄉發生地震　強烈搖晃警戒\\n〈預估強烈搖晃地區〉\\n花蓮　南投　臺東　宜蘭\"},\"eew_alert-general-v2\":{\"title\":\"🚨 《緊急地震速報 (氣象署發布) 》\",\"body\":\"花蓮縣壽豐鄉發生地震　強烈搖晃警戒\\n〈預估強烈搖晃地區〉\\n花蓮　南投　臺東　宜蘭\"},\"eew_alert-silent-v2\":{\"title\":\"🚨 《緊急地震速報 (氣象署發布) 》\",\"body\":\"花蓮縣壽豐鄉發生地震　強烈搖晃警戒\\n〈預估強烈搖晃地區〉\\n花蓮　南投　臺東　宜蘭\"},\"eew-important-v2\":{\"title\":\"⚠️ 地震速報\",\"body\":\"10:15左右，花蓮縣壽豐鄉發生地震。震源深度10公里，地震規模M6.1，最大預估震度4。\"},\"eew-general-v2\":{\"title\":\"⚠️ 地震速報\",\"body\":\"10:15左右，花蓮縣壽豐鄉發生地震。震源深度10公里，地震規模M6.1，最大預估震度4。\"},\"eew-silence-v2\":{\"title\":\"⚠️ 地震速報\",\"body\":\"10:15左右，花蓮縣壽豐鄉發生地震。震源深度10公里，地震規模M6.1，最大預估震度4。\"},\"int_report-general-v2\":{\"title\":\"📨 震度速報 [07:36]\",\"body\":\"[震度 ５弱]　花蓮縣\"},\"int_report-silence-v2\":{\"title\":\"📨 震度速報 [07:36]\",\"body\":\"[震度 ５弱]　花蓮縣\"},\"eq-v2\":{\"title\":\"📡 強震監視器\",\"body\":\"臺南市歸仁區　偵測到晃動\"},\"report-general-v2\":{\"title\":\"🔔 地震報告 [小區域有感地震]\",\"body\":\"00:36左右，花蓮縣近海發生地震。震源深度23.8公里，地震規模M4.0，花蓮縣觀測到最大震度２。\"},\"report-silence-v2\":{\"title\":\"🔔 地震報告 [小區域有感地震]\",\"body\":\"00:36左右，花蓮縣近海發生地震。震源深度23.8公里，地震規模M4.0，花蓮縣觀測到最大震度２。\"},\"thunderstorm-important-v2\":{\"title\":\"⛈️ 山區暴雨\",\"body\":\"您所在區域附近有暴雨發生的機率，留意溪水暴漲並儘速遠離溪流，持續至8/4 16:34\"},\"thunderstorm-general-v2\":{\"title\":\"⛈️ 雷雨即時訊息\",\"body\":\"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至08/26 17:30\"},\"weather_major-important-v2\":{\"title\":\"📊 臺南市歸仁區 天氣特報\",\"body\":\"[發布]超大豪雨特報\"},\"weather_minor-general-v2\":{\"title\":\"📊 臺南市歸仁區 天氣特報\",\"body\":\"[發布]大雨特報\\n對流雲系發展旺盛，易有短延時強降雨，新北市已有豪雨發生，今（７日）晚至明（８日）晨基隆北海岸、彰化、雲林、南投、東半部地區及大臺北山區有局部大雨發生的機率，請注意雷擊及強陣風，山區慎防坍方、落石及溪水暴漲。\"},\"evacuation_major-important-v2\":{\"title\":\"🌧️ 防災資訊(短時極端降雨紀錄)\",\"body\":\"臺南市永康區(CAN040 國一N323K) 1 小時累積雨量達到 91.5 mm/hr，請注意自身安全。\"},\"evacuation_minor-general-v2\":{\"title\":\"⚠️ 防災資訊(河川水位-注意)\",\"body\":\"北寮橋 (水位 73.5m) 已達二級警戒，提高警覺，並密切注意水情變化。\"},\"tsunami-important-v2\":{\"title\":\"🌊 海嘯警報發布\",\"body\":\"海嘯警報已發布\\n請儘速前往安全區域避難\"},\"tsunami-general-v2\":{\"title\":\"🌊 海嘯警報發布\",\"body\":\"海嘯警報已發布\\n請儘速前往安全區域避難\"},\"tsunami-silent-v2\":{\"title\":\"🌊 太平洋海嘯消息\",\"body\":\"頃獲太平洋海嘯警報中心通報，２０２４年０８月１８日０３時１０分（臺灣時間），俄羅斯　堪察加半島東部外海發生規模７﹒４地震，震央位於東經１６０﹒１０度、北緯５２﹒７０度。該中心研判可能在太平洋地區引發海嘯威脅，氣象署將嚴密監視海嘯的後續影響，隨時提供最新資訊。\"},\"announcement-general-v2\":{\"title\":\"📢 公告\",\"body\":\"這是一則測試公告。\"}}"
  },
  {
    "path": "assets/translations/en.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: en\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: English\\n\"\n\"Language: en_US\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"Updating location…\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"Getting GPS location…\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"Receive All\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"Off\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"Category\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"Intensity 1 or higher at current location\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"Tsunami Advisory, Tsunami Warning\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"Tsunami Warning only\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"Receive for current location\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"Intensity 4 or higher at current location\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"Sound Test\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"Announcements\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"When sending an announcement\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"The sound test is a local notification performed on the device, used only to confirm whether sound can be played properly when receiving alerts. This test does not send any requests to any server\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"Queuing, Please Wait...\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"Notifications\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"Push notification settings and notification sound test\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"Location not set\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"Please set your current location to enable notifications\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"Earthquake Warning\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"Earthquake Early Warning\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"Earthquake\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"Earthquake Monitor\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"Reports\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"Earthquake Intensity Report\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"Weather\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"Thunderstorm Advisory\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"Weather Advisory\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"Disaster Prevention Advisory\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"Tsunami\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"Tsunami Advisory\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"Other\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"Severe\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"When a tsunami warning is issued\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"General\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"When a tsunami advisory is issued\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"Pacific Ocean Tsunami Information (Silent Notification)\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"When the Pacific Ocean tsunami advisory is issued\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"Earthquake Monitor (General)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"Shaking detected\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"Intensity Report\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"Local (City/Town/District) measured intensity is greater than 3\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"Intensity Report (Silent)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"Local (City/Town/District) measured intensity is greater than 1\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"Earthquake Report (General)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"Local (Municipality/County) measured intensity is greater than 3\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"Earthquake Report (Silent)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"Local (Municipality/County) measured intensity is greater than 1\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"Updated notification setting\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"Failed To Update Notification Settings\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"Earthquake Early Warning (Severe)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"Maximum intensity is greater than 5- and local (City/Town/District) estimated intensity is greater than 4\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"Earthquake Early Warning (General)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"Maximum intensity is greater than 5- and local (City/Town/District) estimated intensity is greater than 2\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"Earthquake Early Warning (Silent)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"Maximum intensity is greater than 5- and local (City/Town/District) estimated intensity is greater than 1\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"Earthquake Warning (Severe)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"Local (City/Town/District) estimated intensity is greater than 4\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"Earthquake Warning (General)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"Local (City/Town/District) estimated intensity is greater than 2\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"Earthquake Warning (Silent)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"Local (City/Town/District) estimated intensity is greater than 1\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"When the local area(township) issues a disaster prevention alert\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"When the local area(township) issues disaster prevention information\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"The location (township) issues a red signal\\n\"\n\"Weather Warning\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"The location (township) where the above-mentioned exceptions are issued\\n\"\n\"Weather Warning\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"The location (township) issues rainstorm alert in the mountains\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"The location (township) issues instant thunderstorm information\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"Cancel\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"Unit\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"Adjust the units for value in DPIP\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"Use Fahrenheit\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"Language\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"Adjust the display language of DPIP\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"Display Language\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"System Language\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"Help us translate\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"Click here to help us improve the translation of DPIP\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"Translated {translated} ・ Approved {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"Source Language\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"Select Language\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"Unable to connect to the store, please try again later\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"Unable to find the item, please try again later\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"Reload\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"Loading store items\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"Subscription\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/month\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"One-time\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"Restore purchases\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"Unable to connect to the {store}, please try again later.\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"Restoring your purchased subscription\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"Terms of use\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"Privacy policy\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"Settings\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"Location\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"Current Location\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"Set your location to receive updates about your area\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"Interface\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"Theme\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"Adjust the overall appearance and color of DPIP\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"Map\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"Information\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"Release Notes\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"View DPIP's Previous Update Records\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"Third Party Libraries\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"Support Us\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"Help us maintain the stability and long-term development of the server\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"Debug\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"App Version\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"Device Info\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"Copy Notification Token\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"App Logs\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"Unable to obtain Notification Permission\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"Unable to obtain Location Permission\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"Unable to obtain Self-Start Permission\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"Power saving\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"Unable to obtain permission\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"To use the Auto-Location feature, please allow Notification Permission for DPIP in settings, then try again.\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"The automatic location feature requires you to allow DPIP to access your location in order to function properly. Please go to the app settings, find \\\"Location,\\\" and grant permission before trying again.\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"The automatic location feature requires you to always allow DPIP to access your location in order to function properly. Please go to the app settings, find the location permission settings, and select “Always” before trying again.\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"To use automatic location, DPIP needs permission to always access your location. Go to your app settings, enable ‘Always Allow’ for location, and try again.\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"Grant ‘Auto-Start’ permission to let DPIP automatically update your location in the background.\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"Grant ‘Unrestricted’ permission to let DPIP automatically update your location in the background.\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"DPIP needs permission to use automatic location. Go to your app settings and allow the permission, then try again.\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"Auto-Start\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"To get the best DPIP experience, enable Auto-Start to let DPIP receive updates and update your location in the background.\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"To get the best DPIP experience, turn off battery saver to let DPIP receive updates and update your location in the background.\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"Always allow\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"Forever\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"Update Automatically\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"Update your current location periodically\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"The automatic location update feature will use the GPS on your device to update your location based on your geographical position periodically, providing real-time weather and earthquake information, so you can stay up-to-date with the latest local conditions even when DPIP is closed.\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"Notification permission has been denied. Please go to settings to allow it.\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"Battery saver permission has been denied. Please go to settings to allow it.\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"Failed to set your location. Please try again later.\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"Add a new location\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"Special Municipalities/County\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"Current Location\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"Initial Layer\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"Auto Zoom\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"Animation frame rate\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"Theme Mode\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"Light\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"Dark\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"System\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"Severe thunderstorms or heavy rain are occurring near your area. Please take precautions. This will continue until <bold>{time}</bold>.\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"Weather forecast (24 h)\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"Out of service area. Available only in Taiwan\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"Radar\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"No data\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"Confirm\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"Not Set\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"Humidity\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"Wind speed\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"Wind direction\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"Beaufort Scale\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"Air pressure\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"Rain Fall\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"Visibility\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"Gust\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"Gust Scale\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"Sun UVI\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"Nationwide · In Effect\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"Nationwide · Past Events\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"Location · In Effect\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"Location · Past Events\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"Earthquake Early Warning\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"of {serial}\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"At around {time}, an earthquake was felt near <bold>{location}</bold> with an estimated intensity of <bold>M{magnitude}</bold>, with a local felt intensity of <bold>{intensity}</bold>.\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"At around {time}, an earthquake was felt near <bold>{location}</bold> with an estimated intensity of <bold>M{magnitude}</bold> and at a depth of <bold>{depth}</bold>KM.\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"Estimated\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"Waves\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \" Seconds\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"Arrived\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"Updated to {version}\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"Error while retrieving weather data\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"Error retrieving history.\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"Try again\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"Next\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"Disaster Prevention Information Platform\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"Who are we?\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio is a group of mostly students, with an average age of under 20 and a headcount of 15+. Members are students from northern, central and southern Taiwan, Japan, South Korea and China.\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"Our original intention\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"It was first created to gather students passionate about computers and technology. Over time, it grew beyond the school and became what it is today.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"Precautions\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"All information should be considered authoritative only if it is consistent with CWA.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"Depending on the network status, server status, application status, upstream data source status, etc., there is a possibility that information will not be received. We will try our best to avoid such situations, but we cannot guarantee that they will not happen.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"Strong shaking has a chance of reaching the user's location before the notification.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"Earthquake early warning is the result of rapid calculation and may have large errors. It should be understood and used with caution.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"Any action that is not officially approved could lead to legal consequences. It's essential to follow all applicable rules and regulations.\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"Welcome to DPIP\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"DPIP is an app designed by a local Taiwanese team that integrates information from TREM-Net (Taiwan Real-time Earthquake Observation Network) and data from the Central Weather Administration to provide an integrated, single and convenient disaster prevention information application.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"Delivering real-time disaster prevention information via notifications when major disasters occur\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"Use location to automatically update location settings and provide local real-time disaster prevention information\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"Allow DPIP to run continuously in the background for real-time disaster notification information.\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"Save\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"Permission request\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"Users are required to manually go to settings to enable relevant permissions.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"Background Location Access Required\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"To continuing giving the disaster prevention information immediately, DPIP needs the access of location forever.\\n\\n\"\n\"System will guide you to the setting, please select forever location accession.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"Wait\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"Go to settings\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"Permissions\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"We always stand with our users and work hard for their privacy.\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"Map Layers\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"Select the layer you want to display\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"Base Map\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"Simple\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"Monitor\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"Reports\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"Weather\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"Temperature\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"Precipitation\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"Wind/Gust\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"Lightning\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"Units: {unit}\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"Location\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"Long press to set the start point of playback\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"Current time\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"Play start\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"Progress\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"Lightning to ground in 5 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"Lightning to ground in 10 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"Lightning to ground in 30 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"Lightning to ground in 60 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"Lightning in cloud in 5 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"Lightning in cloud in 10 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"Lightning in cloud in 30 min\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"Lightning in cloud in 60 min\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"Today\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10 min\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1 hr\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3 hr\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6 hr\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12 hr\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24 hr\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2 d\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3 d\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"Overseas Station\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"Current Intensity:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"PGA:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"PGV:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"Estimated <bold>M{magnitude}</bold>, max intensity <bold>{intensity}</bold>\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"in {countdown}s\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"Arrived\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"Magnitude<bold>M{magnitude}</bold>, Depth<bold>{depth}</bold>km\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"There are currently no active earthquake early warnings\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"Recent Earthquake Reports\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"More\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"No. {number} Significantly Felt Earthquake\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"Local Earthquake\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"Magnitude\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"Depth\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"Web\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"Event Time\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"Observed Intensities\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"Earthquake Intensity Map Image\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"Intensity Map Image\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"Max PGA Image\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"Max PGV Image\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"Mistake\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"Resolved\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"Impact: Small\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"Impact: Medium\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"Impact: Large\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"Impact: Major\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"Test\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"Change\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"Done\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"Seismic\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"Weather related\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"Unknown\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"There is no announcement.\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"Announcement Details\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"Please go to the app settings and allow the \\\"Photos and Media\\\" permissions and try again.\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"Saved pictures\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"An error occurred while saving the image\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"0\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"1\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"2\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"3\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"4\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"5-\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"5+\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"6-\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"6+\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"Intensity 7\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"Sunny\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"Sunny with Haze\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"Sunny with Mist\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"Sunny with Lightning\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"Sunny with Thunder\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"Sunny with Fog\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"Sunny with Rain\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"Sunny with Rain and Snow\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"Sunny with Heavy Snow\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"Sunny with Snow Grains\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"Sunny with Ice Pellets\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"Sunny with Snow Showers\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"Sunny with Rain and Snow Showers\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"Sunny with Hail\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"Sunny with Thunderstorm\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"Sunny with Thundersnow\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"Clear with thunder and hail\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"Sunny with Heavy Thunderstorms\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"Clear sky with heavy thunder and hail\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"Partly cloudy\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"Cloudy with haze\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"Cloudy with haze\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"Cloudy with Lightning\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"Cloudy with thunder\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"Cloudy with Fog\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"Cloudy with Rain\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"Cloudy with rain and snow\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"Cloudy with heavy snow\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"Cloudy with Snow Pellets\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"Cloudy with Hail\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"Cloudy with Snow Showers\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"Cloudy, Rain and Snow Showers\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"Cloudy with Hail\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"Cloudy with Thunderstorms\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"Cloudy with Thundersnow\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"Cloudy with Hailstorms\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"Cloudy with Thunderstorm\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"Cloudy with Severe Hailstorms\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"Overcast\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"Overcast with Haze\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"Overcast with Mist\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"Overcast with Lighting\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"Cloudy with Thunder\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"Overcast with Fog\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"Overcast with Rain\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"Overcast with Rain/Snow\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"Overcast with Heavy Snow\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"Overcast with Snow\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"Cloudy with ice beads\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"Cloudy with snow showers\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"Cloudy with rain and snow\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"Cloudy with Hail\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"Cloudy with thunderstorm\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"Cloudy with thunder and snow\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"Cloudy with thunder and hail\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"Cloudy with heavy thunderstorm\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"Heavy thunder and hail\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{town} {townLevel}, {city} {cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city} {cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town} {townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/ja.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: ja\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Japanese\\n\"\n\"Language: ja_JP\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"位置を更新中\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"GPS 位置を取得中…\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"すべて受信\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"オフ\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"受信カテゴリ\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"現在地で震度1以上を受信\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"津波警報・情報\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"津波警報のみ受信\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"現在地のみ受信\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"現在地で震度4以上を受信\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"サウンドテスト\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"お知らせ\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"通知受信時\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"サウンドテストは端末上でのみ実行され、通知受信時に正しく音が再生されるかを確認するためだけに使われます。このテストでサーバーにリクエストが送信されることはありません。\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"サーバーで順番待ち中です。しばらくお待ちください…\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"通知\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"プッシュ通知の設定と通知音のテスト\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"現在地が設定されていません\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"通知機能を利用するには、先に現在地を設定してください。\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"地震速報\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"緊急地震速報\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"地震\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"強震モニター\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"地震情報\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"地震速報\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"天気\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"雷雨注意情報\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"天気警報\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"防災情報\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"津波\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"津波情報\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"その他\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"重大\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"津波警報発令中\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"一般\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"津波情報発表時\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"太平洋津波通信（音無しお知らせ）\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"太平洋津波情報発信された時\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"強震モニター（通常）\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"揺れを検出\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"震度速報（通常）\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"現在地（郷／鎮）測りにより震度 3 超え\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"震度速報（音無し）\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"現在地（郷／鎮）測りにより震度 1 超え\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"地震報告（通常）\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"現在地（県／市）測りにより震度 3 超え\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"地震報告（音無し）\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"現在地（県／市）測りにより震度 1 超え\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"通知設定が更新されました\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"通知設定の更新に失敗しました\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"緊急地震速報（非常）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"最大震度 5 弱超え　かつ\\n\"\n\"現在地（郷／鎮）測りにより震度 4 超え\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"緊急地震速報（通常）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"最大震度 5 弱超え　かつ\\n\"\n\"現在地（郷／鎮）測りにより震度 2 超え\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"緊急地震速報（音無し）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"最大震度 5 弱超え　かつ\\n\"\n\"現在地（郷／鎮）測りにより震度 1 超え\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"地震速報（非常）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"現在地（郷／鎮）測りにより震度 4 超え\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"地震速報（通常）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"現在地（郷／鎮）測りにより震度 2 超え\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"地震速報（音無し）\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"現在地（郷／鎮）測りにより震度 1 超え\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"所在地（町・村）で防災警報を発表する時\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"所在地（町）が防災警報を発令した場合\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"現在地（郷／鎮）に赤色灯の気象特別警報が発表されました\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"現在地（郷／鎮）に、上記以外の気象注意報・特別警報が発表されました\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"所在地（町）が山間部の豪雨警報を発令した場合\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"所在地（町）が雷雨のリアルタイム情報を発令した場合\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"キャンセル\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"単位\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"DPIPで数値を表示する際に使用する単位を調整する\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"華氏度を使う\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"言語\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"DPIPの表示言語を調整する\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"表示言語\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"システム言語\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"翻訳を協力\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"DPIPの翻訳にご協力をお願いします！\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"{translated} 翻訳済み・{approved} 校正済み\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"ソース言語\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"言語を選択\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"ストアに接続できません\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"商品が見つかりません\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"再読み込み\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"ストアの商品を読み込み中\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"サブスクリプション\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/毎月\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"一回払い\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"購入を復元\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"{store}に接続できませんでした。しばらく時間をおいてから、もう一度お試しください。\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"ご購入内容を復元しています\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"利用規約\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"プライバシーポリシー\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"設定\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"位置\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"現在地\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"現在地を設定して地域のリアルタイム情報を受け取る\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"画面\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"テーマ\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"DPIPのテーマを調整する\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"地図\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"情報\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"更新履歴\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"過去の更新履歴を見る\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"外部ライブラリの使用許諾\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"応援する\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"サーバーの安定性と長期的な運営の維持の為にご協力ください\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"デバッグ\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"アプリバージョン\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"デバイス情報\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"通知トークンをコピー\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"アプリログ\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"通知の権限を取得できません\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"位置情報の権限を取得できません\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"自動起動の権限を取得できません\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"電力を守る策略\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"権限を取得できませんでした\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"自動位置機能を正常に動作させるには、DPIP に通知の権限を許可する必要があります。アプリの設定で「通知」権限を見つけて許可した後、もう一度お試しください。\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"自動位置機能を正常に動作させるには、DPIP に位置情報の権限を許可する必要があります。アプリの設定で「位置」権限を見つけて許可した後、もう一度お試しください。\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"自動位置機能を正常に動作させるには、DPIP に常に位置情報の権限を許可する必要があります。アプリの設定で位置情報の権限を開き、「常に」を選択した後、もう一度お試しください。\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"自動位置機能を正常に動作させるには、DPIP に位置情報の使用を常に許可する必要があります。アプリの設定で位置情報の権限を開き、「常に許可」を選択した後、もう一度お試しください。\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"より良い自動位置設定体験を得るために、DPIP がバックグラウンドで所在地情報を自動設定できるように、「自動起動の権限」を許可してください。\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"より良い自動位置設定体験を得るために、DPIP がバックグラウンドで所在地情報を自動更新できるよう、動作を「制限なし」にしてください。\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"自動起動\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"常に許可\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"自動更新\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"定期的に現在地を更新する\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"この機能には、デバイスのGPSを利用して、地理的な位置情報に基づいて所在地を更新します。リアルタイムの天気情報や地震情報を提供し、最新の地域状況を取得します。\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"地点を追加\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"県／市\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"現在地\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"初期レイヤー\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"自動ズーム\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"動画のフレームレート\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"テーマモード\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"ライト\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"ダーク\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"システムに従う\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"お住まいの地域付近で激しい雷雨または降雨が発生しています。ご注意ください。<bold>{time}</bold> まで続く見込みです。\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"台湾以外ではご利用いただけません\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"レーダー\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"確認\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"未設定\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"第{serial}報\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"{time}ごろ、<bold>{location}</bold>付近で地震。推定M<bold>{magnitude}</bold>、最大震度<bold>{intensity}</bold>。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"{time}ごろ、<bold>{location}</bold>付近で地震。推定M<bold>{magnitude}</bold>、深さ<bold>{depth}</bold>キロメートル。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"現在地の予想\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"地震波\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \" 秒\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"到達\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"{version}に更新されました\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"天気の取得に失敗しました\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"再試行\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"次\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"防災情報プラットフォーム\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"私たちについて？\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio は殆ど学生で構成され、平均年齢 20 歳未満で人数 15 人超えた団体です。台湾北中南部、日本、韓国と中国の学生たちの組み合いです。\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"私たちの初心\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"設立のきっかけは、コンピュータやテクノロジーに興味と能力を持つ仲間を集めることでした。その後、活動は学外に広がり、現在の形になりました。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"注意事項\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"すべての情報は、中央気象署（CWA）の公式発表を基準にしてください。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"回線やサーバー、アプリケーション、上流データ提供元の状況によっては、情報を受信できない場合があります。私たちはそのような事態を避けるよう最善を尽くしていますが、完全に発生しないことを保証するものではありません。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"強い揺れが通知より早く到達する確率があります。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"地震速報は迅速な計算に基づくため、誤差が大きくなる可能性があります。その点をご理解のうえ、慎重にご利用ください。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"公式に認められていない行為は、法的なリスクを伴う可能性があります。必ず関連する規範を守ってください。\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"DPIPへようこそ\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"DPIPは台湾のチームが開発したアプリで、TREM-Net（台湾即時地震観測網）の情報と中央気象署のデータを統合し、防災情報をひとつにまとめて便利に利用できるアプリケーションです。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"非常災害が発生した時、通知で即時防災情報を伝えます\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"定位により自動的に現在地を更新し、当地である即時防災情報を受けられます\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"DPIPをバックグラウンドで継続的に動作させることで、防災通知を即時に受け取ることができます。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"保存\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"権限を要求\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"ユーザーが手動で設定から該当の権限を有効にする必要があります。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"権限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"私たちは常にユーザーの立場に寄り添い、プライバシーの保護に取り組み続けています。\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"地図レイヤー\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"表示する地図のレイヤーを選択してください\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"ベースマップ\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"シンプル\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"強震モニター\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"地震情報\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"気象\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"気温\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"降水量\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"風向・風速\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"落雷\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"単位：{unit}\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"震源地\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"長タッチして再生の起点を設定する\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"表示中の時刻\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"再生の起点\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"再生中\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"5分間以内の落雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"10分間以内の落雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"30分間以内の落雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"1時間以内の落雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"5分間以内の雲内雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"10分間以内の雲内雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"30分間以内の雲内雷\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"1時間以内の雲内雷\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"今日\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10分間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1時間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3時間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6時間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12時間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24時間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2日間\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3日間\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"M<bold>{magnitude}</bold>、最大震度<bold>{intensity}</bold>\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"到達まであと{countdown}秒\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"到達済み\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"M<bold>{magnitude}</bold>、深さ<bold>{depth}</bold>キロメートル\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"緊急地震速報は発表されていません\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"最近の地震情報\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"さらに表示\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"ID #{number} の顕著な地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"局地的地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"マグニチュード\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"震源の深さ\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"Webで見る\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"発震時刻\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"各地の震度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"地震情報図\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"震度分布図\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"地動最大加速度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"地動最大速度\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"エラーが発生した\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"解決済み\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"影響：小\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"影響：中\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"影響：大\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"メンテナンス\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"テスト\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"変更\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"完成\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"地震相関\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"気象相関\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"不明\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"現在お知らせはありません\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"お知らせの詳細\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"アプリの設定で「写真とメディア」へのアクセス権限を有効にしてから、もう一度お試しください。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"保存しました\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"画像を保存中にエラーが発生しました\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"０\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"１\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"２\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"３\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"４\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"５弱\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"５強\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"６弱\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"６強\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"７\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"晴れ\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"晴れ・煙霧\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"晴れ時々霧\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"晴れ時々雷\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"晴れ時々雷\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"晴れ・霧\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"晴れ時々雨\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"晴れ時々雨雪\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"晴れ時々大雪\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"晴れ時々霧雪\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"晴れ時々氷雨\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"晴れ時々にわか雪\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"晴れ時々にわか雨や雪\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"晴れ時々雹\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"晴れ時々雷を伴う雨\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"晴れのち雪、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"晴れのち雹、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"晴れのち雨、大雷あり\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"晴れのち雹、大雷あり\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"多雲\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"多雲霧もやあり\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"多雲稲妻あり\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"多雲雷あり\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"多雲霧あり\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"多雲雨あり\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"多雲のち雨、雪あり\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"多雲のち大雪\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"多雲のち雪粒\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"多雲のち氷粒\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"多雲のち時々雪\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"多雲のち時々雨、雪あり\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"多雲のち雹\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"多雲のち雨、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"多雲のち雪、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"多雲のち雹、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"多雲のち雨、大雷あり\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"多雲のち雹、大雷あり\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"曇り\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"曇り霧もやあり\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"曇り靄あり\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"曇り稲妻あり\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"曇り雷あり\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"曇り霧あり\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"曇りのち雨\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"曇りのち雨、雪あり\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"曇りのち大雪\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"曇りのち雪粒\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"曇りのち氷粒\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"曇りのち時々雪\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"曇りのち時々雨、雪あり\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"曇りのち雹\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"曇りのち雨、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"曇りのち雪、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"曇りのち雹、雷あり\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"曇りのち雨、大雷あり\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"曇りのち大雹、大雷あり\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{city}{cityLevel} {town}{townLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city}{cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town}{townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/ko.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: ko\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Korean\\n\"\n\"Language: ko_KR\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"위치 갱신중...\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"GPS 위치 수신중...\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"모두 수신\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"닫기\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"수신 카테고리\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"현재 위치 진도 1 이상\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"해일 정보, 해일 경보\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"해일 경보만 수신\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"현재 위치 수신\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"현재 위치 진도 4 이상\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"사운드 테스트\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"공지\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"공지 발송 시\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"사운드 테스트는 기기에서 수행되는 로컬 알림입니다. 기기가 알림을 수신했을 때 음향 효과를 정상적으로 재생할 수 있는지 확인하는 데만 사용됩니다. 이 테스트는 요청을 서버에 전송하지 않습니다\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"서버 대기 중입니다. 잠시만 기다려 주세요…\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"알림\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"푸시 알림 설정 및 알림음 테스트\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"현재 위치가 설정되지 않았습니다\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"알림 기능을 사용하시려면 먼저 위치를 설정해 주세요\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"지진 속보\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"긴급 지진 속보\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"지진\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"강진 모니터\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"지진 보고\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"진도 속보\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"날씨\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"실시간 뇌우 정보\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"기상특보\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"방재 정보\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"해일\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"해일 정보\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"기타\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"중대\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"해일 경보 발령 시\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"일반\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"해일 소식 발표 시\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"태평양 해일 정보(무음 알림)\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"태평양 해일 정보 발령 시\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"강진 모니터(일반)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"흔들림 감지\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"진도 속보(일반)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"현재 위치(향, 진) 실측 진도 3 이상\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"진도 속보(무음 알림)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"현재 위치(향, 진) 실측 진도 1 이상\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"지진 보고(일반)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"현재 위치(현, 시) 실측 진도 3 이상\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"지진 보고(무음 알림)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"현재 위치(현, 시) 실측 진도 1 이상\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"알림 설정 갱신\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"알림 설정 갱신 실패\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"긴급 지진 속보(중대)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"​최대 진도 5 약 이상, 현재 위치(향, 진) 예상 진도 4 이상\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"긴급 지진 속보(일반)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"최대 진도 5 약 이상, 현재 위치(향, 진) 예상 진도 2 이상\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"긴급 지진 속보(무음)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"최대 진도 5 약 이상, 현재 위치(향, 진) 예상 진도 1 이상\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"지진 속보(중대)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"현재 위치(향, 진) 예상 진도 4 이상\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"지진 속보(일반)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"현재 위치(향, 진) 예상 진도 2 이상\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"지진 속보(무음)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"현재 위치(향, 진) 예상 진도 1 이상\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"소재지(향진)에서 재난경보 발령 시\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"지역(현)에서 재난 예방 정보를 발행할 때\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"현재 위치(향, 진)에 적색 기상 경보 특보를 발령\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"현재 위치(향, 진)에 기상 경보를 발령\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"해당 지역(현)의 산악 지역에 폭풍우 경보를 발령합니다.\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"소재지(향ㆍ진)으로 뇌우 실시간 정보\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"취소\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"단위\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"DPIP의 표시 단위 조정\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"화씨 사용\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"언어\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"DPIP 표시 언어 조정\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"표시 언어\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"시스템 언어\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"번역 돕기\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"여기를 눌러 DPIP 번역 개선을 도와주세요\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"번역됨 {translated}・승인됨 {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"출처 언어\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"언어 선택\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"스토어에 연결할 수 없습니다, 잠시 후 다시 시도해 주세요\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"상품을 찾을 수 없습니다, 잠시 후 다시 시도해 주세요\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"새로 고침\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"스토어 상품 로드 중\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"정기 구독\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/월\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"일회성 후원\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"구매 기록 복원\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"{store}에 연결할 수 없습니다, 잠시 후 다시 시도해 주세요.\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"구매했던 구독 복원\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"이용 약관\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"개인 정보 보호 정책\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"설정\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"위치\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"거주지\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"현재 위치를 설정하여 해당 지역 실시간 정보 수신\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"인터페이스\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"주제\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"DPIP 전체의 외관과 색상을 조정합니다\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"지도\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"정보\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"업데이트 로그\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"DPIP 이전 업데이트 기록\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"제3자 라이브러리 라이선스\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"저희를 후원해주세요\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"서버의 안정과 장기적인 발전 돕기\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"오류 수정\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"앱 버전\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"기기 정보\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"알림 토큰 복사\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"앱 로그\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"알림 권한을 얻을 수 없습니다\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"위치 권한을 얻을 수 없습니다\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"자동 시작 권한을 얻을 수 없습니다\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"전원 절약 설정\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"권한 획득 불가\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"자동 위치 설정 기능이 정상적으로 동작하기 위해서는 DPIP가 알림 권한이 필요합니다. 자동 위치 설정 기능을 이용하시려면 설정에서 DPIP의 \\\"알림\\\" 권한을 허용한 다음, 다시 시도해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"자동 위치 설정 기능이 정상적으로 동작하기 위해서는 DPIP가 위치 권한이 필요합니다. 자동 위치 설정 기능을 이용하시려면 설정에서 DPIP의 \\\"위치\\\" 권한을 허용한 다음, 다시 시도해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"자동 위치 설정 기능이 정상적으로 동작하기 위해서는 DPIP가 \\\"항상 허용\\\"되는 위치 권한이 필요합니다. 자동 위치 설정 기능을 이용하시려면 설정에서 DPIP의 위치 권한을 \\\"항상 허용\\\"한 다음, 다시 시도해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"자동 위치 설정 기능이 정상적으로 동작하기 위해서는 DPIP가 \\\"모두 허용\\\"되는 위치 권한이 필요합니다. 자동 위치 설정 기능을 이용하시려면 설정에서 DPIP의 위치 권한을 \\\"모두 허용\\\"한 다음, 다시 시도해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"더 나은 자동 위치 설정을 위해, DPIP가 백그라운드에서 동작할 수 있도록 \\\"자동 실행\\\" 권한을 부여해야 합니다.\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"더 나은 자동 위치 설정을 위해, DPIP가 백그라운드에서 동작할 수 있도록 \\\"제한 없음\\\" 권한을 부여해야 합니다.\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"자동 위치 설정 기능은 DPIP 사용 권한을 허용해야 정상적으로 작동합니다. 앱 설정에서 \\\"권한\\\"을 찾고 다시 시도해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"자동 실행\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"더 나은 DPIP 경험을 위해, DPIP가 백그라운드에서 정보를 수신하고 거주지를 업데이트할 수 있도록 단계에 따라 자동 시작 기능을 활성화하십시오.\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"더 나은 DPIP 경험을 위해, DPIP가 백그라운드에서 정보를 수신하고 거주지를 업데이트할 수 있도록 절전을 비활성화하십시오.\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"항상 허용\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"항상\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"자동 업데이트\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"현재 위치 정기적으로 업데이트\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"자동 위치 기능은 기기의 GPS를 사용하며, DPIP가 꺼져 있거나 사용 중이 아닐 때도 사용자의 위치 정보를 기반으로 현재 위치를 자동으로 업데이트합니다. 이를 통해 실시간 날씨와 지진 정보를 제공하여 사용자가 항상 현지 최신 상황을 파악할 수 있도록 합니다.\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"알림 권한이 거부되었습니다. 설정으로 가서 허용해주세요.\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"배터리 절약 권한이 거부되었습니다. 설정으로 가서 허용해주세요.\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"거주지 설정에 실패하였습니다. 나중에 다시 시도해주세요.\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"위치 추가\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"현/시\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"현재 위치\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"기본 지도\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"자동 확대/축소\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"애니메이션 주사율\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"테마 모드\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"밝은\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"어두운\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"시스템 테마 따르기\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"현재 위치 부근 강한 뇌우 또는 비. <bold>{time}</bold>까지 계속될 수 있으니 주의.\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"일기예보(24시간)\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"서비스 지역 외에서는 사용할 수 없으며, 대만에서만 이용 가능합니다\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"레이더\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"정보 없음\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"확인\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"설정되지 않음\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"습도\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"풍속\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"풍향\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"풍급\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"기압\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"강수량\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"가시거리\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"돌풍\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"돌풍계급\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"자외선\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"전국, 발효중\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"전국, 지남\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"소재지, 발효중\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"소재지, 지남\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"지진속보\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"제 {serial} 보\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"{time}경, <bold>{location}</bold> 부근 체감 지진 발생. 예상 규모 <bold>M{magnitude}</bold>, 현재 위치 최대 진도 <bold>{intensity}</bold>.\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"{time} 경, <bold>{location}</bold>부근에서 유감 지진이 발생, 추정 규모는 <bold>M{magnitude}</bold>、진앙깊이<bold>{depth}</bold>입니다.\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"현재 위치 예상\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"지진파\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \" 초\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"도착\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"{version} 업데이트\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"날씨 이상 정보 확인\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"기록을 불러오던 중 오류가 발생하였습니다.\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"다시 시도해 주세요\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"다음 단계\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"방재 정보 플랫폼\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"저희에 관하여?\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio는 대부분 학생들로 구성되어 있으며, 평균 연령 20 세 미만, 15 명 이상의 단체입니다. 구성원은 대만 북·중·남부, 일본, 한국, 중국의 학생들로 이루어져 있습니다.\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"우리의 목표\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"설립 초기의 목적은 컴퓨터와 기술에 관심과 능력이 있는 학생들을 모집하는 것이었으며, 이후에는 학교 밖으로 발전하여 현재의 모습으로 발전하였습니다.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"주의 사항\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"모든 정보는 대만 기상청에서 발표한 내용을 기준으로 합니다.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"네트워크 상태, 서버 상태, 애플리케이션 상태, 상위 데이터 소스 상태 등에 따라 정보를 받지 못할 가능성이 있습니다. 저희는 이러한 상황을 피하기 위해 최선을 다하지만, 반드시 발생하지 않는다고 보장할 수는 없습니다.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"강한 흔들림이 알림보다 먼저 사용자 위치에 도착할 가능성이 있습니다.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"지진 속보는 신속한 계산 결과이므로, 오차가 클 수 있습니다. 이 점을 이해하고 신중하게 사용해야 합니다.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"공식적으로 인정되지 않는 모든 행위는 법적 위험을 초래할 수 있으니, 관련 규정을 반드시 준수해 주십시오.\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"DPIP에 오신 것을 환영합니다\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"DPIP는 TREM-Net(대만의 실시간 지진 관측 네트워크) 정보 및 중앙 기상청 자료를 통합하여 통합, 단일 및 편리한 방재 정보 응용 프로그램을 제공하는 대만 현지 팀이 설계한 앱입니다.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"중대 재해 발생 시 알림으로 즉각 재난 대비 정보를 전달합니다\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"GPS를 이용하여 소재지 위치를 자동으로 업데이트하고, 현지의 즉각적인 방재 정보를 제공합니다\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"실시간 재난 알림 정보를 위해 DPIP가 백그라운드에서 계속 실행되도록 허용합니다.\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"이미지 저장\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"권한 요청\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"사용자가 수동으로 설정에서 관련 권한을 활성화해야 합니다.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"백그라운드 위치 권한 필요\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"DPIP가 재난 정보를 지속적으로 즉시 제공하기 위해서는 위치 권한을 \\\"항상 허용\\\"해야 합니다.\\n\\n\"\n\"시스템이 설정을 안내해줄 것입니다, 위치 접근을 \\\"항상 허용\\\" 해 주세요.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"대기중...\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"설정하러 가기\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"권한\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"우리는 항상 사용자와 함께하며 사용자의 프라이버시를 위해 끊임없이 노력합니다.\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"지도 레이어\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"표시 지도 레이더 선택\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"배경 레이어\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"간단함\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"모니터\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"지진 정보\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"기상\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"기온\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"강우\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"풍향/풍속\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"번개\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"단위: {unit}\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"에 위치\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"길게 눌러 재생 시작 지점 설정\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"현재 시간\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"재생 시작 지점\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"재생 진행도\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"5분 내 낙뢰\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"10분 내 낙뢰\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"30분 내 낙뢰\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"60분 내 낙뢰\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"5분 내 구내 번개\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"10분 내 구내 번개\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"30분 내 구내 번개\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"60분 내 구내 번개\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"오늘\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10 분\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1 시간\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3 시간\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6 시간\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12 시간\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24 시간\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2 일\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3 일\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"해외 지진계\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"최근 진도:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"PGA:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"PGV:\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"규모 <bold>M{magnitude}</bold>, 현재 위치 예상 진도 <bold>{intensity}</bold>\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"{countdown}초 후 도착\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"도착\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"규모 <bold>M{magnitude}</bold>，깊이<bold>{depth}</bold>km\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"현재 유효 지진 속보 없음\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"최근 지진 현황\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"더 보기\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"번호 {number} 강한 체감 지진\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"소구역 체감 지진\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"지진 규모\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"진원 깊이\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"보고 페이지\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"지진 발생 시간\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"각 지역 진도\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"지진 보고\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"진도\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"최대 지반 가속도\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"최대 지반 속도\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"오류\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"해결 완료\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"영향：작음\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"영향：보통\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"영향：큼\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"유지 보수\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"테스트\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"변경\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"완성\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"지진 관련\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"기상 관련\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"알 수 없음\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"현재 공지 없음\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"공지 내용\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"응용 프로그램 설정에서 '사진 및 미디어' 권한을 찾아 허용 후에 다시 시도해 주세요.\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"이미지 저장 완료\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"이미지 저장 중 오류\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"０급\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"１급\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"２급\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"３급\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"４급\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"５약\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"５강\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"６약\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"６강\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"７급\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"맑음\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"맑음・연무\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"맑음・옅은 안개\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"맑음・번개\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"맑음・천둥\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"맑음・짙은 안개\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"맑음・비\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"맑음・비/눈\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"맑음・폭설\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"맑음・싸라기 눈\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"맑음・진눈깨비\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"맑음・소나기 눈\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"맑음・소나기 비/눈\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"맑음・우박\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"맑음・뇌우\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"맑음・천둥 눈\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"맑음・천둥 우박\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"맑음・강한 뇌우\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"맑음・강한 천둥 우박\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"구름 많음\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"구름 많음・연무\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"구름 많음・옅은 안개\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"구름 많음・번개\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"구름 많음・천둥\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"구름 많음・짙은 안개\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"구름 많음・비\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"구름 많음・비/눈\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"구름 많음・폭설\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"구름 많음・싸라기눈\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"구름 많음・진눈깨비\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"구름 많음・소나기 눈\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"구름 많음・소나기 눈/비\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"구름 많음・우박\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"구름 많음・뇌우\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"구름 많음・천둥/눈\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"구름 많음・천둥/우박\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"구름 많음・강한 뇌우\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"구름 많음・강한 천둥/우박\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"흐림\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"흐림・연무\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"흐림・안개\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"흐림・번개\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"흐림・천둥\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"흐림・옅은 안개\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"흐림・비\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"흐림・눈/비\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"흐림・폭설\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"흐림・싸라기눈\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"흐림・진눈깨비\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"흐림・소나기 눈\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"흐림・소나기 눈/비\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"맑음・우박\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"흐림・뇌우\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"흐림・천둥/눈\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"흐림・천둥/우박\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"흐림・강한 뇌우\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"흐림・강한 천둥/우박\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{city}{cityLevel} {town}{townLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city}{cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town}{townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/location_names.csv",
    "content": "\"key\",\"zh-Hant\",\"en\",\"ja\",\"ko\",\"ru\",\"vi\",\"zh-Hans\"\n\"連江\",\"連江\",\"Lienchiang\",\"連江\",\"롄장\",\"連江\",\"連江\",\"连江\"\n\"宜蘭\",\"宜蘭\",\"Yilan\",\"宜蘭\",\"이란\",\"宜蘭\",\"宜蘭\",\"宜兰\"\n\"彰化\",\"彰化\",\"Changhua\",\"彰化\",\"장화\",\"彰化\",\"彰化\",\"彰化\"\n\"南投\",\"南投\",\"Nantou\",\"南投\",\"난터우\",\"南投\",\"南投\",\"南投\"\n\"雲林\",\"雲林\",\"Yunlin\",\"雲林\",\"윈린\",\"雲林\",\"雲林\",\"云林\"\n\"屏東\",\"屏東\",\"Pingdong\",\"屏東\",\"핑둥\",\"屏東\",\"屏東\",\"屏东\"\n\"基隆\",\"基隆\",\"Keelung\",\"基隆\",\"지룽\",\"基隆\",\"基隆\",\"基隆\"\n\"臺北\",\"臺北\",\"Taipei\",\"台北\",\"타이베이\",\"臺北\",\"臺北\",\"台北\"\n\"新北\",\"新北\",\"New Taipei\",\"新北\",\"신베이\",\"新北\",\"新北\",\"新北\"\n\"臺南\",\"臺南\",\"Tainan\",\"台南\",\"타이난\",\"臺南\",\"臺南\",\"台南\"\n\"桃園\",\"桃園\",\"Taoyuan\",\"桃園\",\"타오위안\",\"桃園\",\"桃園\",\"桃园\"\n\"嘉義\",\"嘉義\",\"Chiayi\",\"嘉義\",\"자이\",\"嘉義\",\"嘉義\",\"嘉义\"\n\"金門\",\"金門\",\"Kinmen\",\"金門\",\"진먼\",\"金門\",\"金門\",\"金门\"\n\"高雄\",\"高雄\",\"Kaohsiung\",\"高雄\",\"가오슝\",\"高雄\",\"高雄\",\"高雄\"\n\"臺東\",\"臺東\",\"Taitung\",\"台東\",\"타이둥\",\"臺東\",\"臺東\",\"台东\"\n\"花蓮\",\"花蓮\",\"Hualien\",\"花蓮\",\"화롄\",\"花蓮\",\"花蓮\",\"花莲\"\n\"澎湖\",\"澎湖\",\"Penghu\",\"澎湖\",\"펑후\",\"澎湖\",\"澎湖\",\"澎湖\"\n\"新竹\",\"新竹\",\"Hsinchu\",\"新竹\",\"신주\",\"新竹\",\"新竹\",\"新竹\"\n\"臺中\",\"臺中\",\"Taichung\",\"台中\",\"타이중\",\"臺中\",\"臺中\",\"台中\"\n\"苗栗\",\"苗栗\",\"Miaoli\",\"苗栗\",\"먀오리\",\"苗栗\",\"苗栗\",\"苗栗\"\n\"成功\",\"成功\",\"Chenggong\",\"成功\",\"성공\",\"成功\",\"成功\",\"成功\"\n\"佳冬\",\"佳冬\",\"Jiadong\",\"佳冬\",\"가동\",\"佳冬\",\"佳冬\",\"佳冬\"\n\"麥寮\",\"麥寮\",\"Mailiao\",\"麦寮\",\"마이랴오\",\"麥寮\",\"麥寮\",\"麦寮\"\n\"綠島\",\"綠島\",\"Ludao\",\"緑島\",\"뤽다오\",\"綠島\",\"綠島\",\"绿岛\"\n\"蘭嶼\",\"蘭嶼\",\"Lanyu\",\"蘭嶼\",\"蘭嶼\",\"蘭嶼\",\"蘭嶼\",\"兰屿\"\n\"田中\",\"田中\",\"Tianzhong\",\"田中\",\"田中\",\"田中\",\"田中\",\"田中\"\n\"社頭\",\"社頭\",\"Shetou\",\"社頭\",\"社頭\",\"社頭\",\"社頭\",\"社头\"\n\"竹田\",\"竹田\",\"Zhutian\",\"竹田\",\"竹田\",\"竹田\",\"竹田\",\"竹田\"\n\"萬丹\",\"萬丹\",\"Wandan\",\"万丹\",\"萬丹\",\"萬丹\",\"萬丹\",\"万丹\"\n\"三灣\",\"三灣\",\"Sanwan\",\"三湾\",\"三灣\",\"三灣\",\"三灣\",\"三湾\"\n\"峨眉\",\"峨眉\",\"Emei\",\"峨眉\",\"峨眉\",\"峨眉\",\"峨眉\",\"峨眉\"\n\"南庄\",\"南庄\",\"Nanzhuang\",\"南庄\",\"南庄\",\"南庄\",\"南庄\",\"南庄\"\n\"太保\",\"太保\",\"Taibao\",\"太保\",\"太保\",\"太保\",\"太保\",\"太保\"\n\"中埔\",\"中埔\",\"Zhongpu\",\"中埔\",\"中埔\",\"中埔\",\"中埔\",\"中埔\"\n\"番路\",\"番路\",\"Fanlu\",\"番路\",\"番路\",\"番路\",\"番路\",\"番路\"\n\"水上\",\"水上\",\"Shuishang\",\"水上\",\"水上\",\"水上\",\"水上\",\"水上\"\n\"員林\",\"員林\",\"Yuanlin\",\"員林\",\"員林\",\"員林\",\"員林\",\"员林\"\n\"小港\",\"小港\",\"Xiaogang\",\"小港\",\"小港\",\"小港\",\"小港\",\"小港\"\n\"蘇澳\",\"蘇澳\",\"Suao\",\"蘇澳\",\"蘇澳\",\"蘇澳\",\"蘇澳\",\"苏澳\"\n\"五結\",\"五結\",\"Wujie\",\"五結\",\"五結\",\"五結\",\"五結\",\"五结\"\n\"壯圍\",\"壯圍\",\"Jhuangwei\",\"壮囲\",\"壯圍\",\"壯圍\",\"壯圍\",\"壮围\"\n\"南竿\",\"南竿\",\"Nangan\",\"南竿\",\"南竿\",\"南竿\",\"南竿\",\"南竿\"\n\"莒光\",\"莒光\",\"Juguang\",\"莒光\",\"莒光\",\"莒光\",\"莒光\",\"莒光\"\n\"烏坵\",\"烏坵\",\"Wuqiu\",\"烏坵\",\"烏坵\",\"烏坵\",\"烏坵\",\"乌坵\"\n\"羅東\",\"羅東\",\"Luodong\",\"羅東\",\"羅東\",\"羅東\",\"羅東\",\"罗东\"\n\"員山\",\"員山\",\"Yuanshan\",\"員山\",\"員山\",\"員山\",\"員山\",\"员山\"\n\"冬山\",\"冬山\",\"Dongshan\",\"冬山\",\"冬山\",\"冬山\",\"冬山\",\"冬山\"\n\"三星\",\"三星\",\"Sanxing\",\"三星\",\"三星\",\"三星\",\"三星\",\"三星\"\n\"大同\",\"大同\",\"Datong\",\"大同\",\"大同\",\"大同\",\"大同\",\"大同\"\n\"竹東\",\"竹東\",\"Zhudong\",\"竹東\",\"竹東\",\"竹東\",\"竹東\",\"竹东\"\n\"新埔\",\"新埔\",\"Sinpu\",\"新埔\",\"新埔\",\"新埔\",\"新埔\",\"新埔\"\n\"關西\",\"關西\",\"Guanxi\",\"関西\",\"關西\",\"關西\",\"關西\",\"关西\"\n\"湖口\",\"湖口\",\"Hukou\",\"湖口\",\"湖口\",\"湖口\",\"湖口\",\"湖口\"\n\"芎林\",\"芎林\",\"Qionglin\",\"芎林\",\"芎林\",\"芎林\",\"芎林\",\"芎林\"\n\"橫山\",\"橫山\",\"Hengshan\",\"横山\",\"橫山\",\"橫山\",\"橫山\",\"横山\"\n\"北埔\",\"北埔\",\"Beipu\",\"北埔\",\"北埔\",\"北埔\",\"北埔\",\"北埔\"\n\"五峰\",\"五峰\",\"Wufeng\",\"五峰\",\"五峰\",\"五峰\",\"五峰\",\"五峰\"\n\"龍井\",\"龍井\",\"Longjing\",\"龍井\",\"룽징\",\"龍井\",\"龍井\",\"龙井\"\n\"大雅\",\"大雅\",\"Daya\",\"大雅\",\"다야\",\"大雅\",\"大雅\",\"大雅\"\n\"沙鹿\",\"沙鹿\",\"Shalu\",\"沙鹿\",\"사루\",\"沙鹿\",\"沙鹿\",\"沙鹿\"\n\"梧棲\",\"梧棲\",\"Wuqi\",\"梧棲\",\"우치\",\"梧棲\",\"梧棲\",\"梧栖\"\n\"湖西\",\"湖西\",\"Husi\",\"湖西\",\"湖西\",\"湖西\",\"湖西\",\"湖西\"\n\"金峰\",\"金峰\",\"Jinfeng\",\"金峰\",\"金峰\",\"金峰\",\"金峰\",\"金峰\"\n\"太麻里\",\"太麻里\",\"Taimali\",\"太麻里\",\"太麻里\",\"太麻里\",\"太麻里\",\"太麻里\"\n\"卓蘭\",\"卓蘭\",\"Zhuolan\",\"卓蘭\",\"卓蘭\",\"卓蘭\",\"卓蘭\",\"卓兰\"\n\"大湖\",\"大湖\",\"Dahu\",\"大湖\",\"大湖\",\"大湖\",\"大湖\",\"大湖\"\n\"公館\",\"公館\",\"Gongguan\",\"公館\",\"公館\",\"公館\",\"公館\",\"公馆\"\n\"銅鑼\",\"銅鑼\",\"Tongluo\",\"銅鑼\",\"銅鑼\",\"銅鑼\",\"銅鑼\",\"铜锣\"\n\"頭屋\",\"頭屋\",\"Touwu\",\"頭屋\",\"頭屋\",\"頭屋\",\"頭屋\",\"头屋\"\n\"三義\",\"三義\",\"Sanyi\",\"三義\",\"三義\",\"三義\",\"三義\",\"三义\"\n\"西湖\",\"西湖\",\"Xihu\",\"西湖\",\"西湖\",\"西湖\",\"西湖\",\"西湖\"\n\"造橋\",\"造橋\",\"Zaoqiao\",\"造橋\",\"造橋\",\"造橋\",\"造橋\",\"造桥\"\n\"獅潭\",\"獅潭\",\"Shitan\",\"獅潭\",\"獅潭\",\"獅潭\",\"獅潭\",\"狮潭\"\n\"和美\",\"和美\",\"Hemei\",\"和美\",\"和美\",\"和美\",\"和美\",\"和美\"\n\"線西\",\"線西\",\"Xianxi\",\"線西\",\"線西\",\"線西\",\"線西\",\"线西\"\n\"伸港\",\"伸港\",\"Shenggang\",\"伸港\",\"伸港\",\"伸港\",\"伸港\",\"伸港\"\n\"秀水\",\"秀水\",\"Xiushui\",\"秀水\",\"秀水\",\"秀水\",\"秀水\",\"秀水\"\n\"花壇\",\"花壇\",\"Huatan\",\"花壇\",\"花壇\",\"花壇\",\"花壇\",\"花坛\"\n\"芬園\",\"芬園\",\"Fenyuan\",\"芬園\",\"芬園\",\"芬園\",\"芬園\",\"芬园\"\n\"溪湖\",\"溪湖\",\"Xihu\",\"渓湖\",\"溪湖\",\"溪湖\",\"溪湖\",\"溪湖\"\n\"東石\",\"東石\",\"Dongshi\",\"東石\",\"東石\",\"東石\",\"東石\",\"东石\"\n\"大村\",\"大村\",\"Dacun\",\"大村\",\"大村\",\"大村\",\"大村\",\"大村\"\n\"埔鹽\",\"埔鹽\",\"Puyan\",\"埔塩\",\"埔鹽\",\"埔鹽\",\"埔鹽\",\"埔盐\"\n\"埔心\",\"埔心\",\"Puxin\",\"埔心\",\"埔心\",\"埔心\",\"埔心\",\"埔心\"\n\"永靖\",\"永靖\",\"Yongjing \",\"永靖\",\"永靖\",\"永靖\",\"永靖\",\"永靖\"\n\"二水\",\"二水\",\"Ershui\",\"二水\",\"二水\",\"二水\",\"二水\",\"二水\"\n\"二林\",\"二林\",\"Erlin\",\"二林\",\"二林\",\"二林\",\"二林\",\"二林\"\n\"埤頭\",\"埤頭\",\"Pitou\",\"埤頭\",\"埤頭\",\"埤頭\",\"埤頭\",\"埤头\"\n\"芳苑\",\"芳苑\",\"Fangyuan\",\"芳苑\",\"芳苑\",\"芳苑\",\"芳苑\",\"芳苑\"\n\"大城\",\"大城\",\"Dacheng\",\"大城\",\"大城\",\"大城\",\"大城\",\"大城\"\n\"竹塘\",\"竹塘\",\"Zhutang\",\"竹塘\",\"竹塘\",\"竹塘\",\"竹塘\",\"竹塘\"\n\"溪州\",\"溪州\",\"Xizhou\",\"渓州\",\"溪州\",\"溪州\",\"溪州\",\"溪洲\"\n\"埔里\",\"埔里\",\"Puli\",\"埔里\",\"埔里\",\"埔里\",\"埔里\",\"埔里\"\n\"草屯\",\"草屯\",\"Caotun\",\"草屯\",\"草屯\",\"草屯\",\"草屯\",\"草屯\"\n\"竹山\",\"竹山\",\"Zhushan\",\"竹山\",\"竹山\",\"竹山\",\"竹山\",\"竹山\"\n\"集集\",\"集集\",\"Jiji\",\"集集\",\"集集\",\"集集\",\"集集\",\"集集\"\n\"名間\",\"名間\",\"Mingjian\",\"名間\",\"名間\",\"名間\",\"名間\",\"名间\"\n\"鹿谷\",\"鹿谷\",\"Lugu\",\"鹿谷\",\"鹿谷\",\"鹿谷\",\"鹿谷\",\"鹿谷\"\n\"中寮\",\"中寮\",\"Zhongliao\",\"中寮\",\"中寮\",\"中寮\",\"中寮\",\"中寮\"\n\"魚池\",\"魚池\",\"Yuchi\",\"魚池\",\"魚池\",\"魚池\",\"魚池\",\"鱼池\"\n\"國姓\",\"國姓\",\"Guoxing\",\"国姓\",\"國姓\",\"國姓\",\"國姓\",\"国姓\"\n\"水里\",\"水里\",\"Shuili\",\"水里\",\"水里\",\"水里\",\"水里\",\"水里\"\n\"信義\",\"信義\",\"Xinyi\",\"信義\",\"信義\",\"信義\",\"信義\",\"信义\"\n\"仁愛\",\"仁愛\",\"Ren'Ai\",\"仁愛\",\"仁愛\",\"仁愛\",\"仁愛\",\"仁爱\"\n\"斗六\",\"斗六\",\"Douliu\",\"斗六\",\"斗六\",\"斗六\",\"斗六\",\"斗六\"\n\"斗南\",\"斗南\",\"Dounan\",\"斗南\",\"斗南\",\"斗南\",\"斗南\",\"鬥南\"\n\"虎尾\",\"虎尾\",\"Huwei\",\"虎尾\",\"虎尾\",\"虎尾\",\"虎尾\",\"虎尾\"\n\"西螺\",\"西螺\",\"Xiluo\",\"西螺\",\"西螺\",\"西螺\",\"西螺\",\"西螺\"\n\"土庫\",\"土庫\",\"Tuku\",\"土庫\",\"土庫\",\"土庫\",\"土庫\",\"土库\"\n\"北港\",\"北港\",\"Beigang\",\"北港\",\"北港\",\"北港\",\"北港\",\"北港\"\n\"古坑\",\"古坑\",\"Gukeng\",\"古坑\",\"古坑\",\"古坑\",\"古坑\",\"古坑\"\n\"大埤\",\"大埤\",\"Dapi\",\"大埤\",\"大埤\",\"大埤\",\"大埤\",\"大埤\"\n\"莿桐\",\"莿桐\",\"Citong\",\"莿桐\",\"莿桐\",\"莿桐\",\"莿桐\",\"莿桐\"\n\"林內\",\"林內\",\"Linnei\",\"林内\",\"林內\",\"林內\",\"林內\",\"林內\"\n\"二崙\",\"二崙\",\"Erlun\",\"二崙\",\"二崙\",\"二崙\",\"二崙\",\"二仑\"\n\"崙背\",\"崙背\",\"Lunbei\",\"崙背\",\"崙背\",\"崙背\",\"崙背\",\"仑背\"\n\"東勢\",\"東勢\",\"Dongshi\",\"東勢\",\"둥스\",\"東勢\",\"東勢\",\"东势\"\n\"褒忠\",\"褒忠\",\"Baozhong\",\"褒忠\",\"褒忠\",\"褒忠\",\"褒忠\",\"褒忠\"\n\"元長\",\"元長\",\"Yuanchang\",\"元長\",\"元長\",\"元長\",\"元長\",\"元长\"\n\"水林\",\"水林\",\"Shuilin\",\"水林\",\"水林\",\"水林\",\"水林\",\"水林\"\n\"朴子\",\"朴子\",\"Puzi\",\"朴子\",\"朴子\",\"朴子\",\"朴子\",\"朴子\"\n\"大林\",\"大林\",\"Dalin\",\"大林\",\"大林\",\"大林\",\"大林\",\"大林\"\n\"民雄\",\"民雄\",\"Minxiong\",\"民雄\",\"民雄\",\"民雄\",\"民雄\",\"民雄\"\n\"溪口\",\"溪口\",\"Xikou\",\"渓口\",\"溪口\",\"溪口\",\"溪口\",\"溪口\"\n\"新港\",\"新港\",\"Xingang\",\"新港\",\"新港\",\"新港\",\"新港\",\"新港\"\n\"六腳\",\"六腳\",\"Liujiao\",\"六脚\",\"六腳\",\"六腳\",\"六腳\",\"六脚\"\n\"義竹\",\"義竹\",\"Yizhu\",\"義竹\",\"義竹\",\"義竹\",\"義竹\",\"义竹\"\n\"鹿草\",\"鹿草\",\"Lucao\",\"鹿草\",\"鹿草\",\"鹿草\",\"鹿草\",\"鹿草\"\n\"竹崎\",\"竹崎\",\"Zhuqi\",\"竹崎\",\"竹崎\",\"竹崎\",\"竹崎\",\"竹崎\"\n\"梅山\",\"梅山\",\"Meishan\",\"梅山\",\"梅山\",\"梅山\",\"梅山\",\"梅山\"\n\"大埔\",\"大埔\",\"Dapu\",\"大埔\",\"大埔\",\"大埔\",\"大埔\",\"大埔\"\n\"阿里山\",\"阿里山\",\"Alishan\",\"阿里山\",\"阿里山\",\"阿里山\",\"阿里山\",\"阿里山\"\n\"潮州\",\"潮州\",\"Chaojhou\",\"潮州\",\"潮州\",\"潮州\",\"潮州\",\"潮州\"\n\"長治\",\"長治\",\"Changzhi\",\"長治\",\"長治\",\"長治\",\"長治\",\"长治\"\n\"麟洛\",\"麟洛\",\"Linluo\",\"麟洛\",\"麟洛\",\"麟洛\",\"麟洛\",\"麟洛\"\n\"九如\",\"九如\",\"Jiuru\",\"九如\",\"九如\",\"九如\",\"九如\",\"九如\"\n\"里港\",\"里港\",\"Ligang\",\"里港\",\"里港\",\"里港\",\"里港\",\"里港\"\n\"鹽埔\",\"鹽埔\",\"Yanpuxinwei\",\"塩埔\",\"鹽埔\",\"鹽埔\",\"鹽埔\",\"盐埔\"\n\"高樹\",\"高樹\",\"Gaoshu\",\"高樹\",\"高樹\",\"高樹\",\"高樹\",\"高树\"\n\"萬巒\",\"萬巒\",\"Wanluan\",\"万巒\",\"萬巒\",\"萬巒\",\"萬巒\",\"万峦\"\n\"內埔\",\"內埔\",\"Neipu\",\"内埔\",\"內埔\",\"內埔\",\"內埔\",\"内埔\"\n\"新埤\",\"新埤\",\"Xinpi\",\"新埤\",\"新埤\",\"新埤\",\"新埤\",\"新埤\"\n\"崁頂\",\"崁頂\",\"Kanding\",\"崁頂\",\"崁頂\",\"崁頂\",\"崁頂\",\"崁顶\"\n\"南州\",\"南州\",\"Nanzhou\",\"南州\",\"南州\",\"南州\",\"南州\",\"南州\"\n\"琉球\",\"琉球\",\"Liuqiu\",\"琉球\",\"琉球\",\"琉球\",\"琉球\",\"琉球\"\n\"三地門\",\"三地門\",\"Sandimen\",\"三地門\",\"三地門\",\"三地門\",\"三地門\",\"三地门\"\n\"霧臺\",\"霧臺\",\"Wutai\",\"霧台\",\"霧臺\",\"霧臺\",\"霧臺\",\"雾台\"\n\"瑪家\",\"瑪家\",\"Majia\",\"瑪家\",\"瑪家\",\"瑪家\",\"瑪家\",\"玛家\"\n\"泰武\",\"泰武\",\"Taiwu\",\"泰武\",\"泰武\",\"泰武\",\"泰武\",\"泰武\"\n\"來義\",\"來義\",\"Laiyi\",\"来義\",\"來義\",\"來義\",\"來義\",\"来义\"\n\"春日\",\"春日\",\"Chunri\",\"春日\",\"春日\",\"春日\",\"春日\",\"春日\"\n\"獅子\",\"獅子\",\"Shizi\",\"獅子\",\"獅子\",\"獅子\",\"獅子\",\"狮子\"\n\"鹿野\",\"鹿野\",\"Luye\",\"鹿野\",\"鹿野\",\"鹿野\",\"鹿野\",\"鹿野\"\n\"池上\",\"池上\",\"Chihshang\",\"池上\",\"池上\",\"池上\",\"池上\",\"池上\"\n\"延平\",\"延平\",\"Yanping\",\"延平\",\"延平\",\"延平\",\"延平\",\"延平\"\n\"光復\",\"光復\",\"Guangfu\",\"光復\",\"光復\",\"光復\",\"光復\",\"光复\"\n\"瑞穗\",\"瑞穗\",\"Ruisui\",\"瑞穂\",\"瑞穗\",\"瑞穗\",\"瑞穗\",\"瑞穗\"\n\"富里\",\"富里\",\"Fuli\",\"富里\",\"富里\",\"富里\",\"富里\",\"富里\"\n\"馬公\",\"馬公\",\"Magong\",\"馬公\",\"馬公\",\"馬公\",\"馬公\",\"马公\"\n\"白沙\",\"白沙\",\"Baisha\",\"白沙\",\"白沙\",\"白沙\",\"白沙\",\"白沙\"\n\"西嶼\",\"西嶼\",\"Xiyu\",\"西嶼\",\"西嶼\",\"西嶼\",\"西嶼\",\"西屿\"\n\"望安\",\"望安\",\"Wangan\",\"望安\",\"望安\",\"望安\",\"望安\",\"望安\"\n\"七美\",\"七美\",\"Qimei\",\"七美\",\"七美\",\"七美\",\"七美\",\"七美\"\n\"暖暖\",\"暖暖\",\"Nuannuan\",\"暖暖\",\"暖暖\",\"暖暖\",\"暖暖\",\"暖暖\"\n\"大安\",\"大安\",\"Da-An\",\"大安\",\"다안\",\"大安\",\"大安\",\"大安\"\n\"文山\",\"文山\",\"Wenshan\",\"文山\",\"文山\",\"文山\",\"文山\",\"文山\"\n\"鹽埕\",\"鹽埕\",\"Yancheng\",\"塩埕\",\"鹽埕\",\"鹽埕\",\"鹽埕\",\"盐埕\"\n\"新興\",\"新興\",\"Sinsing\",\"新興\",\"新興\",\"新興\",\"新興\",\"新兴\"\n\"前金\",\"前金\",\"Qianjin\",\"前金\",\"前金\",\"前金\",\"前金\",\"前金\"\n\"前鎮\",\"前鎮\",\"Chian Jhen\",\"前鎮\",\"前鎮\",\"前鎮\",\"前鎮\",\"前镇\"\n\"頭城\",\"頭城\",\"Toucheng\",\"頭城\",\"頭城\",\"頭城\",\"頭城\",\"头城\"\n\"南澳\",\"南澳\",\"Nanao\",\"南澳\",\"南澳\",\"南澳\",\"南澳\",\"南澳\"\n\"竹北\",\"竹北\",\"Zhubei\",\"竹北\",\"竹北\",\"竹北\",\"竹北\",\"竹北\"\n\"新豐\",\"新豐\",\"Sinfong\",\"新豊\",\"新豐\",\"新豐\",\"新豐\",\"新丰\"\n\"苑裡\",\"苑裡\",\"Yuanli\",\"苑裡\",\"苑裡\",\"苑裡\",\"苑裡\",\"苑里\"\n\"通霄\",\"通霄\",\"Tongxiao\",\"通霄\",\"通霄\",\"通霄\",\"通霄\",\"通宵\"\n\"竹南\",\"竹南\",\"Jhunan\",\"竹南\",\"竹南\",\"竹南\",\"竹南\",\"竹南\"\n\"後龍\",\"後龍\",\"Houlong\",\"後龍\",\"後龍\",\"後龍\",\"後龍\",\"后龙\"\n\"鹿港\",\"鹿港\",\"Lukang\",\"鹿港\",\"鹿港\",\"鹿港\",\"鹿港\",\"鹿港\"\n\"福興\",\"福興\",\"Fuxing\",\"福興\",\"福興\",\"福興\",\"福興\",\"福兴\"\n\"臺西\",\"臺西\",\"Taixi\",\"台西\",\"臺西\",\"臺西\",\"臺西\",\"台西\"\n\"四湖\",\"四湖\",\"Sihu\",\"四湖\",\"四湖\",\"四湖\",\"四湖\",\"四湖\"\n\"口湖\",\"口湖\",\"Kouhu\",\"口湖\",\"口湖\",\"口湖\",\"口湖\",\"口湖\"\n\"布袋\",\"布袋\",\"Budai\",\"布袋\",\"布袋\",\"布袋\",\"布袋\",\"布袋\"\n\"東港\",\"東港\",\"Donggang\",\"東港\",\"東港\",\"東港\",\"東港\",\"东港\"\n\"枋寮\",\"枋寮\",\"Fangliao\",\"枋寮\",\"枋寮\",\"枋寮\",\"枋寮\",\"枋寮\"\n\"新園\",\"新園\",\"Xinyuan\",\"新園\",\"新園\",\"新園\",\"新園\",\"新园\"\n\"林邊\",\"林邊\",\"Linbian\",\"林辺\",\"林邊\",\"林邊\",\"林邊\",\"林边\"\n\"車城\",\"車城\",\"Checheng\",\"車城\",\"車城\",\"車城\",\"車城\",\"车城\"\n\"滿州\",\"滿州\",\"Manzhou\",\"満州\",\"滿州\",\"滿州\",\"滿州\",\"满州\"\n\"枋山\",\"枋山\",\"Fangshan\",\"枋山\",\"枋山\",\"枋山\",\"枋山\",\"枋山\"\n\"牡丹\",\"牡丹\",\"Mudan\",\"牡丹\",\"牡丹\",\"牡丹\",\"牡丹\",\"牡丹\"\n\"卑南\",\"卑南\",\"Beinan\",\"卑南\",\"卑南\",\"卑南\",\"卑南\",\"卑南\"\n\"東河\",\"東河\",\"Donghe\",\"東河\",\"東河\",\"東河\",\"東河\",\"东河\"\n\"吉安\",\"吉安\",\"Ji'an\",\"吉安\",\"吉安\",\"吉安\",\"吉安\",\"吉安\"\n\"壽豐\",\"壽豐\",\"Shoufeng\",\"寿豊\",\"壽豐\",\"壽豐\",\"壽豐\",\"寿丰\"\n\"秀林\",\"秀林\",\"Xiulin\",\"秀林\",\"秀林\",\"秀林\",\"秀林\",\"秀林\"\n\"楠梓\",\"楠梓\",\"Nanzi\",\"楠梓\",\"楠梓\",\"楠梓\",\"楠梓\",\"楠梓\"\n\"鳳山\",\"鳳山\",\"Fengshan\",\"鳳山\",\"鳳山\",\"鳳山\",\"鳳山\",\"凤山\"\n\"大寮\",\"大寮\",\"Daliao\",\"大寮\",\"大寮\",\"大寮\",\"大寮\",\"大寮\"\n\"大樹\",\"大樹\",\"Dashu\",\"大樹\",\"大樹\",\"大樹\",\"大樹\",\"大树\"\n\"大社\",\"大社\",\"Dashe\",\"大社\",\"大社\",\"大社\",\"大社\",\"大社\"\n\"仁武\",\"仁武\",\"Renwu\",\"仁武\",\"仁武\",\"仁武\",\"仁武\",\"仁武\"\n\"鳥松\",\"鳥松\",\"Niaosong\",\"鳥松\",\"鳥松\",\"鳥松\",\"鳥松\",\"鸟松\"\n\"岡山\",\"岡山\",\"Gangshan\",\"岡山\",\"岡山\",\"岡山\",\"岡山\",\"冈山\"\n\"橋頭\",\"橋頭\",\"Qiaotou\",\"橋頭\",\"橋頭\",\"橋頭\",\"橋頭\",\"桥头\"\n\"燕巢\",\"燕巢\",\"Yanchao\",\"燕巣\",\"燕巢\",\"燕巢\",\"燕巢\",\"燕巢\"\n\"田寮\",\"田寮\",\"Tianliao\",\"田寮\",\"田寮\",\"田寮\",\"田寮\",\"田寮\"\n\"阿蓮\",\"阿蓮\",\"Alian\",\"阿蓮\",\"阿蓮\",\"阿蓮\",\"阿蓮\",\"阿连\"\n\"路竹\",\"路竹\",\"Luzhu\",\"路竹\",\"路竹\",\"路竹\",\"路竹\",\"路竹\"\n\"湖內\",\"湖內\",\"Hunei\",\"湖内\",\"湖內\",\"湖內\",\"湖內\",\"湖内\"\n\"旗山\",\"旗山\",\"Qishan\",\"旗山\",\"旗山\",\"旗山\",\"旗山\",\"旗山\"\n\"美濃\",\"美濃\",\"Meinong\",\"美濃\",\"美濃\",\"美濃\",\"美濃\",\"美浓\"\n\"六龜\",\"六龜\",\"Liugui\",\"六亀\",\"六龜\",\"六龜\",\"六龜\",\"六龟\"\n\"甲仙\",\"甲仙\",\"Jiaxian\",\"甲仙\",\"甲仙\",\"甲仙\",\"甲仙\",\"甲仙\"\n\"杉林\",\"杉林\",\"Shanlin\",\"杉林\",\"杉林\",\"杉林\",\"杉林\",\"杉林\"\n\"內門\",\"內門\",\"Neimen\",\"内門\",\"內門\",\"內門\",\"內門\",\"内门\"\n\"茂林\",\"茂林\",\"Maolin\",\"茂林\",\"茂林\",\"茂林\",\"茂林\",\"茂林\"\n\"桃源\",\"桃源\",\"Taoyuan\",\"桃源\",\"桃源\",\"桃源\",\"桃源\",\"桃源\"\n\"那瑪夏\",\"那瑪夏\",\"Namaxia\",\"那瑪夏\",\"那瑪夏\",\"那瑪夏\",\"那瑪夏\",\"那玛夏\"\n\"永和\",\"永和\",\"Yonghe\",\"永和\",\"永和\",\"永和\",\"永和\",\"永和\"\n\"新店\",\"新店\",\"Xindian\",\"新店\",\"新店\",\"新店\",\"新店\",\"新店\"\n\"土城\",\"土城\",\"Tucheng\",\"土城\",\"土城\",\"土城\",\"土城\",\"土城\"\n\"蘆洲\",\"蘆洲\",\"Lujhou\",\"蘆洲\",\"蘆洲\",\"蘆洲\",\"蘆洲\",\"芦洲\"\n\"五股\",\"五股\",\"Wugu\",\"五股\",\"五股\",\"五股\",\"五股\",\"五股\"\n\"坪林\",\"坪林\",\"Pinglin\",\"坪林\",\"坪林\",\"坪林\",\"坪林\",\"坪林\"\n\"平溪\",\"平溪\",\"Pingxi\",\"平渓\",\"平溪\",\"平溪\",\"平溪\",\"平溪\"\n\"烏來\",\"烏來\",\"Wulai\",\"烏来\",\"烏來\",\"烏來\",\"烏來\",\"乌来\"\n\"豐原\",\"豐原\",\"Fengyuan\",\"豊原\",\"펑위안\",\"豐原\",\"豐原\",\"丰原\"\n\"后里\",\"后里\",\"Houli\",\"后里\",\"허우리\",\"后里\",\"后里\",\"后里\"\n\"神岡\",\"神岡\",\"Shengang\",\"神岡\",\"룽징\",\"神岡\",\"神岡\",\"神冈\"\n\"新社\",\"新社\",\"Xinshe\",\"新社\",\"신서\",\"新社\",\"新社\",\"新社\"\n\"石岡\",\"石岡\",\"Shigang\",\"石岡\",\"스강\",\"石岡\",\"石岡\",\"石冈\"\n\"外埔\",\"外埔\",\"Waipu\",\"外埔\",\"와이푸\",\"外埔\",\"外埔\",\"外埔\"\n\"大肚\",\"大肚\",\"Dadu\",\"大肚\",\"다두\",\"大肚\",\"大肚\",\"大肚\"\n\"新營\",\"新營\",\"Xinying\",\"新営\",\"新營\",\"新營\",\"新營\",\"新营\"\n\"鹽水\",\"鹽水\",\"Yanshui\",\"塩水\",\"鹽水\",\"鹽水\",\"鹽水\",\"盐水\"\n\"白河\",\"白河\",\"Baihe\",\"白河\",\"白河\",\"白河\",\"白河\",\"白河\"\n\"後壁\",\"後壁\",\"Houbi\",\"後壁\",\"後壁\",\"後壁\",\"後壁\",\"后壁\"\n\"麻豆\",\"麻豆\",\"Madou\",\"麻豆\",\"麻豆\",\"麻豆\",\"麻豆\",\"麻豆\"\n\"下營\",\"下營\",\"Xiaying\",\"下営\",\"下營\",\"下營\",\"下營\",\"下营\"\n\"六甲\",\"六甲\",\"Liujia\",\"六甲\",\"六甲\",\"六甲\",\"六甲\",\"六甲\"\n\"官田\",\"官田\",\"Guantian\",\"官田\",\"官田\",\"官田\",\"官田\",\"官田\"\n\"大內\",\"大內\",\"Danei\",\"大内\",\"大內\",\"大內\",\"大內\",\"大内\"\n\"佳里\",\"佳里\",\"Jiali\",\"佳里\",\"佳里\",\"佳里\",\"佳里\",\"佳里\"\n\"學甲\",\"學甲\",\"Xuejia\",\"学甲\",\"學甲\",\"學甲\",\"學甲\",\"学甲\"\n\"西港\",\"西港\",\"Xigang\",\"西港\",\"西港\",\"西港\",\"西港\",\"西港\"\n\"新化\",\"新化\",\"Xinhua\",\"新化\",\"新化\",\"新化\",\"新化\",\"新化\"\n\"新市\",\"新市\",\"Xinshi\",\"新市\",\"新市\",\"新市\",\"新市\",\"新市\"\n\"安定\",\"安定\",\"Anding\",\"安定\",\"安定\",\"安定\",\"安定\",\"安定\"\n\"玉井\",\"玉井\",\"Yujing\",\"玉井\",\"玉井\",\"玉井\",\"玉井\",\"玉井\"\n\"楠西\",\"楠西\",\"Nanxi\",\"楠西\",\"楠西\",\"楠西\",\"楠西\",\"楠西\"\n\"南化\",\"南化\",\"Nanhua\",\"南化\",\"南化\",\"南化\",\"南化\",\"南化\"\n\"左鎮\",\"左鎮\",\"Zuozhen\",\"左鎮\",\"左鎮\",\"左鎮\",\"左鎮\",\"左镇\"\n\"仁德\",\"仁德\",\"Rende\",\"仁徳\",\"仁德\",\"仁德\",\"仁德\",\"仁德\"\n\"歸仁\",\"歸仁\",\"Guiren\",\"帰仁\",\"歸仁\",\"歸仁\",\"歸仁\",\"归仁\"\n\"關廟\",\"關廟\",\"Guanmiao\",\"関廟\",\"關廟\",\"關廟\",\"關廟\",\"关庙\"\n\"龍崎\",\"龍崎\",\"Longqi\",\"龍崎\",\"龍崎\",\"龍崎\",\"龍崎\",\"龙崎\"\n\"永康\",\"永康\",\"Yongkang\",\"永康\",\"永康\",\"永康\",\"永康\",\"永康\"\n\"北\",\"北\",\"North\",\"北\",\"베이\",\"北\",\"北\",\"北\"\n\"林園\",\"林園\",\"Linyuan\",\"林園\",\"林園\",\"林園\",\"林園\",\"林园\"\n\"茄萣\",\"茄萣\",\"Qieding\",\"茄萣\",\"茄萣\",\"茄萣\",\"茄萣\",\"茄萣\"\n\"永安\",\"永安\",\"Yong'An\",\"永安\",\"永安\",\"永安\",\"永安\",\"永安\"\n\"彌陀\",\"彌陀\",\"Mituo\",\"弥陀\",\"彌陀\",\"彌陀\",\"彌陀\",\"弥陀\"\n\"梓官\",\"梓官\",\"Ziguan\",\"梓官\",\"梓官\",\"梓官\",\"梓官\",\"梓官\"\n\"淡水\",\"淡水\",\"Tamsui\",\"淡水\",\"淡水\",\"淡水\",\"淡水\",\"淡水\"\n\"瑞芳\",\"瑞芳\",\"Rueifang\",\"瑞芳\",\"瑞芳\",\"瑞芳\",\"瑞芳\",\"瑞芳\"\n\"林口\",\"林口\",\"Linkou\",\"林口\",\"林口\",\"林口\",\"林口\",\"林口\"\n\"三芝\",\"三芝\",\"Sanzhi\",\"三芝\",\"三芝\",\"三芝\",\"三芝\",\"三芝\"\n\"八里\",\"八里\",\"Bali\",\"八里\",\"八里\",\"八里\",\"八里\",\"八里\"\n\"大甲\",\"大甲\",\"Dajia\",\"大甲\",\"다자\",\"大甲\",\"大甲\",\"大甲\"\n\"北門\",\"北門\",\"Beimen\",\"北門\",\"北門\",\"北門\",\"北門\",\"北门\"\n\"安南\",\"安南\",\"Annan\",\"安南\",\"安南\",\"安南\",\"安南\",\"安南\"\n\"蘆竹\",\"蘆竹\",\"Luzhu\",\"蘆竹\",\"蘆竹\",\"蘆竹\",\"蘆竹\",\"芦竹\"\n\"龜山\",\"龜山\",\"Guishan\",\"亀山\",\"龜山\",\"龜山\",\"龜山\",\"龟山\"\n\"復興\",\"復興\",\"Fuxing\",\"復興\",\"復興\",\"復興\",\"復興\",\"复兴\"\n\"東\",\"東\",\"East\",\"東\",\"둥\",\"東\",\"東\",\"东\"\n\"西\",\"西\",\"West\",\"西\",\"시\",\"西\",\"西\",\"西\"\n\"達仁\",\"達仁\",\"Daren\",\"達仁\",\"達仁\",\"達仁\",\"達仁\",\"达仁\"\n\"大武\",\"大武\",\"Dawu\",\"大武\",\"大武\",\"大武\",\"大武\",\"大武\"\n\"關山\",\"關山\",\"Guanshan\",\"関山\",\"關山\",\"關山\",\"關山\",\"关山\"\n\"海端\",\"海端\",\"Haiduan\",\"海端\",\"海端\",\"海端\",\"海端\",\"海端\"\n\"香山\",\"香山\",\"Xiangshan\",\"香山\",\"香山\",\"香山\",\"香山\",\"香山\"\n\"礁溪\",\"礁溪\",\"Chiaoshi\",\"礁渓\",\"礁溪\",\"礁溪\",\"礁溪\",\"礁溪\"\n\"玉里\",\"玉里\",\"Yuli\",\"玉里\",\"玉里\",\"玉里\",\"玉里\",\"玉里\"\n\"卓溪\",\"卓溪\",\"Zhuoxi\",\"卓渓\",\"卓溪\",\"卓溪\",\"卓溪\",\"卓溪\"\n\"頭份\",\"頭份\",\"Toufen\",\"頭份\",\"頭份\",\"頭份\",\"頭份\",\"头份\"\n\"清水\",\"清水\",\"Qingshui\",\"清水\",\"清水\",\"清水\",\"清水\",\"清水\"\n\"南\",\"南\",\"South\",\"南\",\"난\",\"南\",\"南\",\"南\"\n\"安平\",\"安平\",\"Anping\",\"安平\",\"安平\",\"安平\",\"安平\",\"安平\"\n\"中西\",\"中西\",\"West Central\",\"中西\",\"中西\",\"中西\",\"中西\",\"中西\"\n\"大溪\",\"大溪\",\"Dasi\",\"大渓\",\"大溪\",\"大溪\",\"大溪\",\"大溪\"\n\"八德\",\"八德\",\"Bade\",\"八徳\",\"八德\",\"八德\",\"八德\",\"八德\"\n\"大園\",\"大園\",\"Dayuan\",\"大園\",\"大園\",\"大園\",\"大園\",\"大园\"\n\"楊梅\",\"楊梅\",\"Yangmei\",\"楊梅\",\"楊梅\",\"楊梅\",\"楊梅\",\"杨梅\"\n\"七堵\",\"七堵\",\"Qidu\",\"七堵\",\"七堵\",\"七堵\",\"七堵\",\"七堵\"\n\"中正\",\"中正\",\"Zhongzheng\",\"中正\",\"中正\",\"中正\",\"中正\",\"中正\"\n\"中山\",\"中山\",\"Zhongshan\",\"中山\",\"中山\",\"中山\",\"中山\",\"中山\"\n\"安樂\",\"安樂\",\"Anle\",\"安楽\",\"安樂\",\"安樂\",\"安樂\",\"安乐\"\n\"三峽\",\"三峽\",\"Sanshia\",\"三峡\",\"三峽\",\"三峽\",\"三峽\",\"三峡\"\n\"鶯歌\",\"鶯歌\",\"Yingge\",\"鶯歌\",\"鶯歌\",\"鶯歌\",\"鶯歌\",\"莺歌\"\n\"中和\",\"中和\",\"Zhonghe\",\"中和\",\"中和\",\"中和\",\"中和\",\"中和\"\n\"樹林\",\"樹林\",\"Shulin\",\"樹林\",\"樹林\",\"樹林\",\"樹林\",\"树林\"\n\"深坑\",\"深坑\",\"Shenkeng\",\"深坑\",\"深坑\",\"深坑\",\"深坑\",\"深坑\"\n\"板橋\",\"板橋\",\"Banchiao\",\"板橋\",\"板橋\",\"板橋\",\"板橋\",\"板桥\"\n\"石碇\",\"石碇\",\"Shihding\",\"石碇\",\"石碇\",\"石碇\",\"石碇\",\"石碇\"\n\"新莊\",\"新莊\",\"Xinzhuang\",\"新荘\",\"新莊\",\"新莊\",\"新莊\",\"新庄\"\n\"泰山\",\"泰山\",\"Taishan\",\"泰山\",\"泰山\",\"泰山\",\"泰山\",\"泰山\"\n\"三重\",\"三重\",\"Sanchong\",\"三重\",\"三重\",\"三重\",\"三重\",\"三重\"\n\"雙溪\",\"雙溪\",\"Shuangsi\",\"双渓\",\"雙溪\",\"雙溪\",\"雙溪\",\"双溪\"\n\"貢寮\",\"貢寮\",\"Gongliao\",\"貢寮\",\"貢寮\",\"貢寮\",\"貢寮\",\"贡寮\"\n\"汐止\",\"汐止\",\"Xizhi\",\"汐止\",\"汐止\",\"汐止\",\"汐止\",\"汐止\"\n\"萬里\",\"萬里\",\"Wanli\",\"万里\",\"萬里\",\"萬里\",\"萬里\",\"万里\"\n\"金山\",\"金山\",\"Jinshan\",\"金山\",\"金山\",\"金山\",\"金山\",\"金山\"\n\"石門\",\"石門\",\"Shimen\",\"石門\",\"石門\",\"石門\",\"石門\",\"石门\"\n\"苓雅\",\"苓雅\",\"Lingya\",\"苓雅\",\"苓雅\",\"苓雅\",\"苓雅\",\"苓雅\"\n\"三民\",\"三民\",\"Sanmin\",\"三民\",\"三民\",\"三民\",\"三民\",\"三民\"\n\"新屋\",\"新屋\",\"Xinwu\",\"新屋\",\"新屋\",\"新屋\",\"新屋\",\"新屋\"\n\"觀音\",\"觀音\",\"Guanyin\",\"観音\",\"觀音\",\"觀音\",\"觀音\",\"观音\"\n\"北竿\",\"北竿\",\"Beigan\",\"北竿\",\"北竿\",\"北竿\",\"北竿\",\"北竿\"\n\"東引\",\"東引\",\"Dongyin\",\"東引\",\"東引\",\"東引\",\"東引\",\"东引\"\n\"烈嶼\",\"烈嶼\",\"Lieyu\",\"烈嶼\",\"烈嶼\",\"烈嶼\",\"烈嶼\",\"烈屿\"\n\"旗津\",\"旗津\",\"Qijin\",\"旗津\",\"旗津\",\"旗津\",\"旗津\",\"旗津\"\n\"長濱\",\"長濱\",\"Changbin\",\"長浜\",\"長濱\",\"長濱\",\"長濱\",\"长滨\"\n\"豐濱\",\"豐濱\",\"Fengbin\",\"豊浜\",\"豐濱\",\"豐濱\",\"豐濱\",\"丰滨\"\n\"霧峰\",\"霧峰\",\"Wufeng\",\"霧峰\",\"우펑\",\"霧峰\",\"霧峰\",\"雾峰\"\n\"大里\",\"大里\",\"Dali\",\"大里\",\"다리\",\"大里\",\"大里\",\"大里\"\n\"烏日\",\"烏日\",\"Wuri\",\"烏日\",\"우르\",\"烏日\",\"烏日\",\"乌日\"\n\"中\",\"中\",\"Central\",\"中\",\"중\",\"中\",\"中\",\"中\"\n\"南屯\",\"南屯\",\"Nantun\",\"南屯\",\"난툰\",\"南屯\",\"南屯\",\"南屯\"\n\"西屯\",\"西屯\",\"Xitun\",\"西屯\",\"시툰\",\"西屯\",\"西屯\",\"西屯\"\n\"北屯\",\"北屯\",\"Beitun\",\"北屯\",\"베이툰\",\"北屯\",\"北屯\",\"北屯\"\n\"潭子\",\"潭子\",\"Tanzi\",\"潭子\",\"탄쯔\",\"潭子\",\"潭子\",\"潭子\"\n\"萬華\",\"萬華\",\"Wanhua\",\"万華\",\"萬華\",\"萬華\",\"萬華\",\"万华\"\n\"松山\",\"松山\",\"Songshan\",\"松山\",\"松山\",\"松山\",\"松山\",\"松山\"\n\"士林\",\"士林\",\"Shilin\",\"士林\",\"士林\",\"士林\",\"士林\",\"士林\"\n\"北投\",\"北投\",\"Beitou\",\"北投\",\"北投\",\"北投\",\"北投\",\"北投\"\n\"新城\",\"新城\",\"Xincheng\",\"新城\",\"新城\",\"新城\",\"新城\",\"新城\"\n\"善化\",\"善化\",\"Shanhua\",\"善化\",\"善化\",\"善化\",\"善化\",\"善化\"\n\"山上\",\"山上\",\"Shanshang\",\"山上\",\"山上\",\"山上\",\"山上\",\"山上\"\n\"北斗\",\"北斗\",\"Beidou\",\"北斗\",\"北斗\",\"北斗\",\"北斗\",\"北斗\"\n\"田尾\",\"田尾\",\"Tianwei\",\"田尾\",\"田尾\",\"田尾\",\"田尾\",\"田尾\"\n\"金沙\",\"金沙\",\"Jinsha \",\"金沙\",\"金沙\",\"金沙\",\"金沙\",\"金沙\"\n\"金湖\",\"金湖\",\"Jinhu\",\"金湖\",\"金湖\",\"金湖\",\"金湖\",\"金湖\"\n\"柳營\",\"柳營\",\"Liuying\",\"柳営\",\"柳營\",\"柳營\",\"柳營\",\"柳营\"\n\"東山\",\"東山\",\"Dongshan\",\"東山\",\"東山\",\"東山\",\"東山\",\"东山\"\n\"七股\",\"七股\",\"Qigu\",\"七股\",\"七股\",\"七股\",\"七股\",\"七股\"\n\"將軍\",\"將軍\",\"Jiangjun\",\"将軍\",\"將軍\",\"將軍\",\"將軍\",\"将军\"\n\"鼓山\",\"鼓山\",\"Gushan\",\"鼓山\",\"鼓山\",\"鼓山\",\"鼓山\",\"鼓山\"\n\"左營\",\"左營\",\"Zuoying\",\"左営\",\"左營\",\"左營\",\"左營\",\"左营\"\n\"中壢\",\"中壢\",\"Zhongli\",\"中壢\",\"中壢\",\"中壢\",\"中壢\",\"中坜\"\n\"寶山\",\"寶山\",\"Baoshan\",\"宝山\",\"寶山\",\"寶山\",\"寶山\",\"宝山\"\n\"恆春\",\"恆春\",\"Hengchun\",\"恒春\",\"恆春\",\"恆春\",\"恆春\",\"恒春\"\n\"太平\",\"太平\",\"Taiping\",\"太平\",\"太平\",\"太平\",\"太平\",\"太平\"\n\"鳳林\",\"鳳林\",\"Fenglin\",\"鳳林\",\"鳳林\",\"鳳林\",\"鳳林\",\"凤林\"\n\"萬榮\",\"萬榮\",\"Wanrong\",\"万栄\",\"萬榮\",\"萬榮\",\"萬榮\",\"万荣\"\n\"龍潭\",\"龍潭\",\"Longtan\",\"龍潭\",\"龍潭\",\"龍潭\",\"龍潭\",\"龙潭\"\n\"平鎮\",\"平鎮\",\"Pingjhen\",\"平鎮\",\"平鎮\",\"平鎮\",\"平鎮\",\"平镇\"\n\"南港\",\"南港\",\"Nangang\",\"南港\",\"南港\",\"南港\",\"南港\",\"南港\"\n\"內湖\",\"內湖\",\"Neihu\",\"内湖\",\"內湖\",\"內湖\",\"內湖\",\"内湖\"\n\"金寧\",\"金寧\",\"Jinning\",\"金寧\",\"金寧\",\"金寧\",\"金寧\",\"金宁\"\n\"金城\",\"金城\",\"Jincheng\",\"金城\",\"金城\",\"金城\",\"金城\",\"金城\"\n\"尖石\",\"尖石\",\"Jianshi\",\"尖石\",\"尖石\",\"尖石\",\"尖石\",\"尖石\"\n\"泰安\",\"泰安\",\"Tai-An\",\"泰安\",\"泰安\",\"泰安\",\"泰安\",\"泰安\"\n\"和平\",\"和平\",\"Heping\",\"和平\",\"허핑\",\"和平\",\"和平\",\"和平\"\n\"縣\",\"縣\",\"County\",\"県\",\"현\",\"縣\",\"縣\",\"县\"\n\"鄉\",\"鄉\",\"Township\",\"郷\",\"향\",\"鄉\",\"鄉\",\"乡\"\n\"鎮\",\"鎮\",\"Town\",\"鎮\",\"진\",\"鎮\",\"鎮\",\"镇\"\n\"市\",\"市\",\"City\",\"市\",\"시\",\"市\",\"市\",\"市\"\n\"區\",\"區\",\"District\",\"区\",\"구\",\"區\",\"區\",\"区\"\n"
  },
  {
    "path": "assets/translations/ru.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: ru\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Russian\\n\"\n\"Language: ru_RU\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"Уведомление\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"Экстренное предупреждение о землетрясении\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"Система слежения за сильными толчками\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"Отчет о землетрясении\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"Срочное предупреждение о магнитуде землетрясения\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"Единица\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"Язык\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"Переведено {translated}・Утверждено {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"Местоположение\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"Тема\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"Карта\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"Добавить место\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"Ясно\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"Ясно, дымка\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"Ясно, туман\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"Ясно, молния\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"Ясно, гром\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"Ясно, туман\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"Ясно, дождь\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"Ясно, дождь со снегом\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"Ясно, сильный снег\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"Ясно, снежная крупа\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"Ясно, ледяной дождь\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"Ясно, снежные заряды\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"Ясно, дождь со снегом\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"Ясно, град\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"Ясно, гроза\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"Ясно, дымка\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"Ясно, туман\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"Ясно, гром\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"Ясно, туман\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"Ясно, дождь\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"Ясно, град\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/vi.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: vi\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Vietnamese\\n\"\n\"Language: vi_VN\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"Đang cập nhật vị trí\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"Đang tải vị trí GPS...\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"Nhận tất cả\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"Tắt\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"Cài đặt thông báo\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"Cường độ tại địa phương từ 1 trở lên\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"Tin tức và cảnh báo sóng thần\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"Chỉ nhận được cảnh báo sóng thần\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"Vị trí cảnh báo\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"Cường độ tại địa phương từ 4 trở lên\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"Kiểm tra âm thanh\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"Thông báo\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"Khi gửi thông báo\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"kiểm tra âm thanh chỉ thực hiện trên thiết bị này, mục địch để xác nhận thiết bị có âm thanh thông báo. Không thu thập dữ liệu\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"Máy chủ đang bận, vui lòng đợi...\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"Thông báo đẩy\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"Cài đặt thông báo đẩy và kiểm tra âm thanh\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"Bạn chưa thiết lập vị trí của mình\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"Vui lòng thiết lập vị trí để nhận thông báo.\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"Cảnh báo động đất sớm\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"Cảnh báo động đất sớm\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"Động đất\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"Giám sát động đất\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"Báo cáo động đất\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"Cảnh báo nhanh về cường độ địa chấn\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"Thời tiết\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"Thông báo giông sét\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"Cảnh báo thời tiết\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"Thông tin phòng chống thiên tai\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"Sóng thần\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"Thông tin sóng thần\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"Khác\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"Nghiêm trọng\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"Khi có cảnh báo sóng thần\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"Nói chung\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"Khi có thông tin sóng thần\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"Tin tức về sóng thần Thái Bình Dương (Thông báo im lặng)\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"Khi tin tức về sóng thần Thái Bình Dương được công bố\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"Giám sát động đất mạnh(Chung)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"Phát hiện rung chuyển\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"Báo cáo chớp nhoáng cường độ địa chấn (Chung)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"Cường độ thực đo tại địa phương (thị trấn) từ 3 trở lên\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"Báo cáo chớp nhoáng cường độ địa chấn (Thông báo im lặng)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"Cường độ thực đo tại địa phương (thị trấn) từ 1 trở lên\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"Báo cáo động đất (Chung)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"Cường độ thực đo tại địa phương (Quận và thành phố) từ 3 trở lên\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"Báo cáo động đất (Thông báo im lặng)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"Cường độ thực đo tại địa phương (Quận và thành phố) từ 1 trở lên\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"Đã cập nhật cài đặt thông báo\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"Cập nhật cài đặt thông báo thất bại\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"Cảnh báo động đất khẩn cấp (Lớn)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"Cường độ tối đa từ 5 yếu trở lên\\n\"\n\"và dự đoán tại địa phương (thị trấn) từ 4 trở lên\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"Cảnh báo động đất (Nhỏ)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"Cường độ tối đa từ 5 yếu trở lên\\n\"\n\"và dự đoán tại địa phương (thị trấn) từ 2 trở lên\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"Cảnn báo động đất (Im Lặng)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"Cường độ tối đa từ 5 yếu trở lên\\n\"\n\"và dự đoán tại địa phương (thị trấn) từ 1 trở lên\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"Báo cáo chớp nhoáng động đất (Lớn)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"Dự đoán cường độ tại địa phương (thị trấn) từ 4 trở lên\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"Cảnh báo sớm về động đất (Chung)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"Dự đoán cường độ tại địa phương (thị trấn) từ 2 trở lên\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"Báo cáo nhanh về động đất (im lặng)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"Dự đoán cường độ tại địa phương (thị trấn) từ 1 trở lên\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"Khu vực địa phương (thị trấn) ban hành báo cáo đặc biệt cảnh báo thời tiết tín hiệu đỏ\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"Khu vực địa phương (thị trấn) ban hành báo cáo đặc biệt cảnh báo thời tiết tín hiệu\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"Hủy bỏ\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"Đơn vị\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"Thấy đổi đơn vị đo lường DPIP\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"Sử dụng độ F\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"Ngôn ngữ\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"Thay đổi ngôn ngữ hiển thị DPIP\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"Ngôn ngữ hiển thị\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"Ngôn ngữ hệ thống\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"Hỗ trợ biên dịch\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"Nhấp vào đây để giúp chúng tôi cải thiện bản dịch DPIP\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"Đã dịch {translated}・Đã hiệu đính {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"Ngôn ngữ nguồn\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"Chọn ngôn ngữ\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"Không thể tải nhật ký thay đổi, vui lòng thử lại sau.\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"Không tìm thấy sản phẩm, vui lòng thử lại\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"Tải lại trang\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"Đang tải danh sách\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"Gói đăng ký\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/Tháng\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"Ủng hộ một lần\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"Khôi phục giao dịch\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"Khôi phục gói đăng ký của bạn\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"Điều khoản sử dụng\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"Chính sách bảo mật\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"Cài đặt\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"Vị trí\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"Vị Trí Hiện Tại\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"Thiết lập vị trí để nhận thông tin địa phương theo thời gian thực\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"Giáo diện\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"Chủ đề\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"Tùy chỉnh diện mạo và màu sắc DPIP\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"Bản đồ\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"Thông tin\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"Nhật ký thay đổi\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"Lịch sử cập nhật DPIP\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"Giấy phép gói phần mềm của bên thứ ba\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"Quyên tặng\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"Hỗ trợ chúng tôi duy trì sự ổn định và phát triển lâu dài của các máy chủ của chúng tôi\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"Gỡ lỗi\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"Phiên bản ứng dụng\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"Thông tin thiết bị\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"Đã sao chép Token thông báo\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"Nhật ký ứng dụng\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"Chưa cấp quyền thông báo\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"Chưa cấp quyền vị trí\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"Chưa bật quyền tự khởi chạy\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"Chế độ nguồn điện thấp\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"Yêu cầu quyền thất bại\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"Tính năng tự động định vị yêu cầu quyền thông báo để hoạt động chính xác. Vui lòng đi tới cài đặt ứng dụng, tìm và cho phép quyền \\\"Thông báo\\\", sau đó thử lại.\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"Vui lòng cấp quyền vị trí để sử dụng tính năng tự động định vị. Hãy bật \\\"Vị trí\\\" trong cài đặt ứng dụng và thử lại.\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"Để tự động định vị, vui lòng cấp quyền vị trí ở mức \\\"Luôn cho phép\\\". Hãy vào cài đặt ứng dụng, chọn quyền Vị trí và tích vào \\\"Luôn cho phép\\\" rồi thử lại.\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"Vui lòng cấp quyền vị trí \\\"Luôn cho phép\\\" để sử dụng tính năng tự động định vị. Hãy vào cài đặt ứng dụng và chọn \\\"Luôn cho phép\\\" trong mục Vị trí rồi thử lại.\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"Để tính năng tự động định vị hoạt động tốt hơn, bạn cần cấp \\\"Quyền tự khởi chạy\\\" để DPIP có thể tự động cập nhật vị trí khi chạy ngầm.\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"Để trải nghiệm tính năng tự động định vị tốt hơn, bạn cần cấp quyền \\\"Không hạn chế\\\" để DPIP có thể cập nhật vị trí chính xác khi chạy ngầm.\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"Vui lòng cấp quyền để sử dụng tính năng tự động định vị. Hãy bật \\\"Quyền\\\" trong cài đặt ứng dụng và thử lại.\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"Tự khởi chạy\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"Vui lòng làm theo hướng dẫn để bật \\\"Tự khởi chạy\\\". Việc này giúp DPIP cập nhật thông tin và vị trí của bạn ngay cả khi ứng dụng đang chạy nền.\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"Để có trải nghiệm DPIP tốt hơn, vui lòng làm theo các bước để tắt \\\"Chế độ tiết kiệm pin\\\". Việc này giúp DPIP nhận thông tin và cập nhật vị trí chính xác khi chạy ngầm.\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"Luôn cho phép\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"Luôn luôn\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"Cập nhật tự động\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"Cập nhật vị trí hiện tại định kỳ\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"Tính năng định vị tự động sẽ sử dụng GPS trên thiết bị của bạn để tự động cập nhật vị trí của bạn dựa trên vị trí địa lý, cung cấp thông tin thời tiết và động đất theo thời gian thực, giúp bạn luôn nắm bắt được tình hình mới nhất tại địa phương.\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"Thêm địa điểm mới\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"Quận\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"Giao diện chủ đề\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"Sáng\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"Tối\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"Theo chủ đề hệ thống\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"Khu vực gần bạn đang có mưa giông hoặc mưa lớn. Vui lòng chú ý đề phòng. Tình trạng này kéo dài đến <bold>{time}</bold>.\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"Ngoài khu vực dịch vụ, chỉ có sẵn ở các địa điểm khác nhau trên khắp Đài Loan.\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"Hình ảnh radar\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"Xác nhận\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"Chưa đặt\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"Không thể lấy dữ liệu thời tiết\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"Tiếp theo\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"Cổng thông tin phòng chống thiên tai\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"Chúng tôi là ai?\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio là một nhóm gồm hơn 15 Học sinh, phần lớn là những người có độ tuổi trung bình dưới 20. Các thành viên đến từ Đài Loan (Bắc, Trung và Nam), Nhật Bản, Hàn Quốc và Trung Quốc.\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"Chúng tôi đã bắt đầu với những gì\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"Nhóm ban đầu được thành lập bằng cách tuyển dụng một nhóm học sinh có sở thích và năng khiếu về máy tính và công nghệ, sau đó đã phát triển vượt ra ngoài phạm vi trường học và trở thành nhóm như hiện nay.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"Những điều cần lưu ý\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"Tất cả thông tin phải tuân theo nội dung do Đài Loan Cơ quan Thời tiết Trung ương (CWA) công bố.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"Tùy thuộc vào trạng thái mạng, trạng thái máy chủ, trạng thái ứng dụng, trạng thái nguồn dữ liệu ngược dòng, v.v., sự cố không nhận được thông tin có thể xảy ra và chúng tôi sẽ cố gắng hết sức để tránh những tình huống như vậy, nhưng chúng tôi không đảm bảo rằng chúng sẽ không xảy ra.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"Rung lắc mạnh có xu hướng xảy ra ở vị trí của người dùng sớm hơn thông báo.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"Báo cáo động đất nhanh là những tính toán nhanh và có thể chứa nhiều lỗi, do đó cần phải thận trọng và hiểu rõ.\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"Bất kỳ hành vi nào không được chính thức cho phép đều có thể bị coi là vi phạm pháp luật của Trung Hoa Dân Quốc (Đài Loan), vì vậy hãy đảm bảo tuân thủ các quy định có liên quan.\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"Chào mừng đến với DPIP\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"DPIP là ứng dụng được thiết kế bởi một nhóm địa phương tại Đài Loan, tích hợp thông tin từ TREM-Net (Mạng quan sát động đất thời gian thực Đài Loan) và dữ liệu từ Đài Loan Cơ quan thời tiết trung ương (CWA) để cung cấp một ứng dụng thông tin phòng chống thiên tai tích hợp, duy nhất và tiện lợi.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"Dịch vụ được sử dụng để gửi thông báo cảnh báo thiên tai khẩn cấp\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"Dịch vụ được sử dụng để cập nhật vị trí và cung cấp thông tin cảnh báo tại khu vực hiện tại\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"Cho phép DPIP tiếp tục hoạt động ở chế độ nền để thông báo phòng ngừa thảm họa ngay lập tức.\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"Lưu trữ hình\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"Yêu cầu để xin phép\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"Người dùng cần phải tự tay vào trang cài đặt để mở các quyền có liên quan.\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"Để liên tục cung cấp thông tin phòng chống thiên tai tức thời khi chạy ngầm, DPIP cần quyền vị trí ở chế độ \\\"Luôn cho phép\\\".\\n\\n\"\n\"​Hệ thống sẽ dẫn bạn đến trang cài đặt, vui lòng chọn tùy chọn \\\"Luôn cho phép\\\".\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"Sự cho phép\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"Chúng tôi luôn ủng hộ người dùng và nỗ lực không ngừng vì quyền riêng tư của họ.\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"Lớp bản đồ\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"Chọn lớp bản đồ cần hiển thị\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"H\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"Thời tiết\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"Nhiệt độ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"Lượng mưa\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"Hướng gió/Tốc độ gió\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"nằm ở\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"Hôm nay\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10 phút\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1 giờ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3 giờ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6 giờ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12 giờ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24 giờ\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2 ngày\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3 ngày\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"Báo cáo động đất gần đây\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"Thêm\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"Động đất cảm nhận tại khu vực nhỏ\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"Cường độ\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"Độ sâu chấn tiêu\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"Trang báo cáo\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"Ngày giờ nhận biết động đất\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"Cường độ địa chấn tại các khu vực\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"Bản đồ báo cáo động đất\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"Bản đồ cường độ địa chấn\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"Bản đồ gia tốc mặt đất tối đa\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"Bản đồ vận tốc mặt đất tối đa\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"Đã lưu ảnh\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"Đã xảy ra lỗi khi lưu ảnh\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"Cấp độ０\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"Cấp độ１\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"Cấp độ２\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"Cấp độ３\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"Cấp độ４\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"Cấp５mạnh\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"Cấp６yếu\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"Cấp６mạnh\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"Cấp độ７\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"Mùa hè\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"Nhiều mây\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"Ngày nhiều mây\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{city}{cityLevel} {town}{townLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city}{cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town}{townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/weather_station_names.csv",
    "content": "\"key\",\"zh-Hant\",\"en\",\"ja\",\"ko\",\"ru\",\"vi\",\"zh-Hans\"\n\"466850\",\"五分山雷達站\",\"Wufenshan\",\"五分山レーダー局\",\"五分山雷達站\",\"五分山雷達站\",\"五分山雷達站\",\"五分山雷达站\"\n\"466881\",\"新北\",\"New Taipei\",\"新北\",\"신베이\",\"新北\",\"新北\",\"新北\"\n\"466900\",\"淡水\",\"Tamsui\",\"淡水\",\"淡水\",\"淡水\",\"淡水\",\"淡水\"\n\"466910\",\"鞍部\",\"Anbu\",\"鞍部\",\"鞍部\",\"鞍部\",\"鞍部\",\"鞍部\"\n\"466920\",\"臺北\",\"Taipei\",\"台北\",\"타이베이\",\"臺北\",\"臺北\",\"台北\"\n\"466930\",\"竹子湖\",\"Zhuzihu\",\"竹子湖\",\"竹子湖\",\"竹子湖\",\"竹子湖\",\"竹子湖\"\n\"466940\",\"基隆\",\"Keelung\",\"基隆\",\"지룽\",\"基隆\",\"基隆\",\"基隆\"\n\"466950\",\"彭佳嶼\",\"Pengjiayu\",\"彭佳嶼\",\"彭佳嶼\",\"彭佳嶼\",\"彭佳嶼\",\"彭佳屿\"\n\"466990\",\"花蓮\",\"Hualien\",\"花蓮\",\"화롄\",\"花蓮\",\"花蓮\",\"花莲\"\n\"467050\",\"新屋\",\"Xinwu\",\"新屋\",\"新屋\",\"新屋\",\"新屋\",\"新屋\"\n\"467080\",\"宜蘭\",\"Yilan\",\"宜蘭\",\"이란\",\"宜蘭\",\"宜蘭\",\"宜兰\"\n\"467110\",\"金門\",\"Kinmen\",\"金門\",\"진먼\",\"金門\",\"金門\",\"金门\"\n\"467270\",\"田中\",\"Tianzhong\",\"田中\",\"田中\",\"田中\",\"田中\",\"田中\"\n\"467280\",\"後龍\",\"Houlong\",\"後龍\",\"後龍\",\"後龍\",\"後龍\",\"后龙\"\n\"467290\",\"古坑\",\"Gukeng\",\"古坑\",\"古坑\",\"古坑\",\"古坑\",\"古坑\"\n\"467300\",\"東吉島\",\"Dongjidao\",\"東吉島\",\"東吉島\",\"東吉島\",\"東吉島\",\"东吉岛\"\n\"467350\",\"澎湖\",\"Penghu\",\"澎湖\",\"펑후\",\"澎湖\",\"澎湖\",\"澎湖\"\n\"467410\",\"臺南\",\"Tainan\",\"台南\",\"타이난\",\"臺南\",\"臺南\",\"台南\"\n\"467420\",\"永康\",\"Yongkang\",\"永康\",\"永康\",\"永康\",\"永康\",\"永康\"\n\"467441\",\"高雄\",\"Kaohsiung\",\"高雄\",\"가오슝\",\"高雄\",\"高雄\",\"高雄\"\n\"467480\",\"嘉義\",\"Chiayi\",\"嘉義\",\"자이\",\"嘉義\",\"嘉義\",\"嘉义\"\n\"467490\",\"臺中\",\"Taichung\",\"台中\",\"타이중\",\"臺中\",\"臺中\",\"台中\"\n\"467530\",\"阿里山\",\"Alishan\",\"阿里山\",\"阿里山\",\"阿里山\",\"阿里山\",\"阿里山\"\n\"467540\",\"大武\",\"Dawu\",\"大武\",\"大武\",\"大武\",\"大武\",\"大武\"\n\"467550\",\"玉山\",\"Yushan\",\"玉山\",\"玉山\",\"玉山\",\"玉山\",\"玉山\"\n\"467571\",\"新竹\",\"Hsinchu\",\"新竹\",\"신주\",\"新竹\",\"新竹\",\"新竹\"\n\"467590\",\"恆春\",\"Hengchun\",\"恒春\",\"恆春\",\"恆春\",\"恆春\",\"恒春\"\n\"467610\",\"成功\",\"Chenggong\",\"成功\",\"성공\",\"成功\",\"成功\",\"成功\"\n\"467620\",\"蘭嶼\",\"Lanyu\",\"蘭嶼\",\"蘭嶼\",\"蘭嶼\",\"蘭嶼\",\"兰屿\"\n\"467650\",\"日月潭\",\"Sun Moon Lake\",\"日月潭\",\"日月潭\",\"日月潭\",\"日月潭\",\"日月潭\"\n\"467660\",\"臺東\",\"Taitung\",\"台東\",\"타이둥\",\"臺東\",\"臺東\",\"台东\"\n\"467790\",\"墾丁雷達站\",\"Kenting\",\"墾丁レーダー局\",\"墾丁雷達站\",\"墾丁雷達站\",\"墾丁雷達站\",\"垦丁雷达站\"\n\"467990\",\"馬祖\",\"Matsu\",\"馬祖\",\"馬祖\",\"馬祖\",\"馬祖\",\"马祖\"\n\"12J990\",\"口湖工作站\",\"Kouhu Branch Station\",\"口湖工作站\",\"口湖工作站\",\"口湖工作站\",\"口湖工作站\",\"口湖工作站\"\n\"12Q970\",\"東港工作站\",\"Donggang Branch\",\"東港工作站\",\"東港工作站\",\"東港工作站\",\"東港工作站\",\"东港工作站\"\n\"12Q980\",\"恆春工作站\",\"Hengchun Branch\",\"恆春工作站\",\"恆春工作站\",\"恆春工作站\",\"恆春工作站\",\"恒春工作站\"\n\"42HA10\",\"萬大發電廠\",\"Wanta Hydro\",\"萬大発電所\",\"萬大發電廠\",\"萬大發電廠\",\"萬大發電廠\",\"万大发电厂\"\n\"72AI40\",\"桃改樹林分場\",\"Shulin Sub-station Of Taoyuan ARES\",\"桃改樹林分場\",\"桃改樹林分場\",\"桃改樹林分場\",\"桃改樹林分場\",\"桃改树林分场\"\n\"72C440\",\"桃園農改場\",\"Taoyuan Agricultural Research Center\",\"桃園農改場\",\"桃園農改場\",\"桃園農改場\",\"桃園農改場\",\"桃园农改场\"\n\"72D080\",\"桃改五峰分場\",\"Wufeng Sub-station Of Taoyuan ARES\",\"桃改五峰分場\",\"桃改五峰分場\",\"桃改五峰分場\",\"桃改五峰分場\",\"桃改五峰分场\"\n\"72D680\",\"桃改新埔分場\",\"Sinpu Sub-station Of Taoyuan ARES\",\"桃改新埔分場\",\"桃改新埔分場\",\"桃改新埔分場\",\"桃改新埔分場\",\"桃改新埔分场\"\n\"72G600\",\"臺中農改場\",\"Ta Tsun\",\"臺中農改場\",\"臺中農改場\",\"臺中農改場\",\"臺中農改場\",\"台中农改场\"\n\"72HA00\",\"中改埔里分場\",\"Puli Sub-station Of Taichung ARES\",\"中改埔里分場\",\"中改埔里分場\",\"中改埔里分場\",\"中改埔里分場\",\"中改埔里分场\"\n\"72K220\",\"南改斗南分場\",\"Dounan Sub-station Of Tainan ARES\",\"南改斗南分場\",\"南改斗南分場\",\"南改斗南分場\",\"南改斗南分場\",\"南改斗南分场\"\n\"72M360\",\"南改義竹分場\",\"Yichu Sub-station Of Tainan ARES\",\"南改義竹分場\",\"南改義竹分場\",\"南改義竹分場\",\"南改義竹分場\",\"南改义竹分场\"\n\"72M700\",\"南改鹿草分場\",\"Lucao Sub-station Of Tainan ARES\",\"南改鹿草分場\",\"南改鹿草分場\",\"南改鹿草分場\",\"南改鹿草分場\",\"南改鹿草分场\"\n\"72N100\",\"臺南農改場\",\"Tainan\",\"臺南農改場\",\"臺南農改場\",\"臺南農改場\",\"臺南農改場\",\"台南农改场\"\n\"72N240\",\"七股研究中心\",\"Qigu Research Center\",\"七股研究中心\",\"七股研究中心\",\"七股研究中心\",\"七股研究中心\",\"七股研究中心\"\n\"72Q010\",\"高雄農改場\",\"Pingtung\",\"高雄農改場\",\"高雄農改場\",\"高雄農改場\",\"高雄農改場\",\"高雄农改场\"\n\"72S200\",\"東改班鳩分場\",\"Banjuou Sub-station Of Taitung ARES \",\"東改班鳩分場\",\"東改班鳩分場\",\"東改班鳩分場\",\"東改班鳩分場\",\"东改斑鸠分场\"\n\"72S590\",\"東改賓朗果園\",\"Binlung Orchard Sub-station Of Taitung ARES \",\"東改賓朗果園（東部農業改良場・賓朗果園）\",\"東改賓朗果園\",\"東改賓朗果園\",\"東改賓朗果園\",\"东改宾朗果园\"\n\"72T250\",\"花蓮農改場\",\"Gi An\",\"花蓮農改場\",\"花蓮農改場\",\"花蓮農改場\",\"花蓮農改場\",\"花莲农改场\"\n\"72U480\",\"花改蘭陽分場\",\"Lanyang Sub-station Of Hualien ARES\",\"花改蘭陽分場\",\"花改蘭陽分場\",\"花改蘭陽分場\",\"花改蘭陽分場\",\"花改兰阳分场\"\n\"72V140\",\"高改旗南分場\",\"Chinan Sub-station Of Kaohsiung ARES\",\"高改旗南分場\",\"高改旗南分場\",\"高改旗南分場\",\"高改旗南分場\",\"高改旗南分场\"\n\"82A750\",\"茶改北部分場\",\"North Branch Of TBRS\",\"茶改北部分場\",\"茶改北部分場\",\"茶改北部分場\",\"茶改北部分場\",\"茶改北部分场\"\n\"82C160\",\"茶改場\",\"Tea And Beverage Research Station\",\"茶改場\",\"茶改場\",\"茶改場\",\"茶改場\",\"茶改场\"\n\"82H320\",\"茶改中部分場\",\"Middle Branch Of TRES\",\"茶改中部分場\",\"茶改中部分場\",\"茶改中部分場\",\"茶改中部分場\",\"茶改中部分场\"\n\"82H840\",\"茶改南部分場\",\"South Branch Of TBRS\",\"茶改南部分場\",\"茶改南部分場\",\"茶改南部分場\",\"茶改南部分場\",\"茶改南部分场\"\n\"82S580\",\"茶改東部分場\",\"East Branch Of TBRS\",\"茶改東部分場\",\"茶改東部分場\",\"茶改東部分場\",\"茶改東部分場\",\"茶改东部分场\"\n\"A2C560\",\"農工中心\",\"Agricultural Engineering Research Center\",\"農工中心\",\"農工中心\",\"農工中心\",\"農工中心\",\"农工中心\"\n\"A2K360\",\"水試臺西試驗場\",\"Taixi Experimental Fishing Ground\",\"水試臺西実験場\",\"水試臺西試驗場\",\"水試臺西試驗場\",\"水試臺西試驗場\",\"水改台西试验场\"\n\"A2K630\",\"臺大雲林校區\",\"Yunlin Branch Of NTU\",\"台大雲林校区\",\"臺大雲林校區\",\"臺大雲林校區\",\"臺大雲林校區\",\"台大云林校区\"\n\"A2N290\",\"臺南蘭花園區\",\"Taiwan Orchid\",\"臺南蘭花園區\",\"臺南蘭花園區\",\"臺南蘭花園區\",\"臺南蘭花園區\",\"台南兰花园区\"\n\"B2E890\",\"畜試北區分所\",\"North Branch Of TLRI\",\"畜試北區分所\",\"畜試北區分所\",\"畜試北區分所\",\"畜試北區分所\",\"蓄试北区分所\"\n\"B2N890\",\"畜產試驗所\",\"Hsin Hua\",\"畜產実験所\",\"畜產試驗所\",\"畜產試驗所\",\"畜產試驗所\",\"畜产试验所\"\n\"B2Q810\",\"畜試南區分所\",\"South Branch Of TLRI\",\"畜試南區分所\",\"畜試南區分所\",\"畜試南區分所\",\"畜試南區分所\",\"蓄试南区分所\"\n\"B2U990\",\"畜試東區分所\",\"East Branch Of TLRI\",\"畜試東區分所\",\"畜試東區分所\",\"畜試東區分所\",\"畜試東區分所\",\"蓄试东区分所\"\n\"C2A540\",\"四堵\",\"Sihdu\",\"四堵\",\"四堵\",\"四堵\",\"四堵\",\"四堵\"\n\"C2A560\",\"福山\",\"Fushan\",\"福山\",\"福山\",\"福山\",\"福山\",\"福山\"\n\"C2A650\",\"火燒寮\",\"Huoshaoliao\",\"火燒寮\",\"火燒寮\",\"火燒寮\",\"火燒寮\",\"火烧寮\"\n\"C2A660\",\"瑞芳\",\"Rueifang\",\"瑞芳\",\"瑞芳\",\"瑞芳\",\"瑞芳\",\"瑞芳\"\n\"C2A880\",\"福隆\",\"Fulong\",\"福隆\",\"福隆\",\"福隆\",\"福隆\",\"福隆\"\n\"C2A920\",\"富貴角\",\"Fugueijiao\",\"富貴角\",\"富貴角\",\"富貴角\",\"富貴角\",\"富贵角\"\n\"C2C410\",\"中央大學\",\"Ncu\",\"中央大学\",\"中央大學\",\"中央大學\",\"中央大學\",\"中央大学\"\n\"C2D720\",\"關西工作站\",\"Guanxi Experiment Station\",\"関西工作站\",\"關西工作站\",\"關西工作站\",\"關西工作站\",\"关西工作站\"\n\"C2D730\",\"寶山農場\",\"Baoshan Farm\",\"宝山農場\",\"寶山農場\",\"寶山農場\",\"寶山農場\",\"宝山农场\"\n\"C2D740\",\"屯原\",\"Tunyuan\",\"屯原\",\"屯原\",\"屯原\",\"屯原\",\"屯原\"\n\"C2E520\",\"大湖\",\"Dahu\",\"大湖\",\"大湖\",\"大湖\",\"大湖\",\"大湖\"\n\"C2E540\",\"龍溪\",\"Longxi\",\"龍溪\",\"龍溪\",\"龍溪\",\"龍溪\",\"龙溪\"\n\"C2E880\",\"三義\",\"Sanyi\",\"三義\",\"三義\",\"三義\",\"三義\",\"三义\"\n\"C2E970\",\"八甲\",\"Bajia\",\"八甲\",\"八甲\",\"八甲\",\"八甲\",\"八甲\"\n\"C2F000\",\"大肚\",\"Dadu\",\"大肚\",\"다두\",\"大肚\",\"大肚\",\"大肚\"\n\"C2F860\",\"梨山\",\"Lishan\",\"梨山\",\"梨山\",\"梨山\",\"梨山\",\"梨山\"\n\"C2F930\",\"大甲\",\"Dajia\",\"大甲\",\"다자\",\"大甲\",\"大甲\",\"大甲\"\n\"C2F990\",\"摩天嶺\",\"Motianling\",\"摩天嶺\",\"摩天嶺\",\"摩天嶺\",\"摩天嶺\",\"摩天岭\"\n\"C2F9A0\",\"中竹林\",\"Zhongzhulin\",\"中竹林\",\"中竹林\",\"中竹林\",\"中竹林\",\"中竹林\"\n\"C2FA00\",\"烏石坑\",\"Wushihkeng\",\"烏石坑\",\"烏石坑\",\"烏石坑\",\"烏石坑\",\"乌石坑\"\n\"C2FB50\",\"出雲\",\"Chuyun\",\"出雲\",\"出雲\",\"出雲\",\"出雲\",\"出云\"\n\"C2FB60\",\"頭櫃山\",\"Touguishan\",\"頭櫃山\",\"頭櫃山\",\"頭櫃山\",\"頭櫃山\",\"头柜山\"\n\"C2G640\",\"鹿港\",\"Lukang\",\"鹿港\",\"鹿港\",\"鹿港\",\"鹿港\",\"鹿港\"\n\"C2G840\",\"北斗\",\"Beidou\",\"北斗\",\"北斗\",\"北斗\",\"北斗\",\"北斗\"\n\"C2G870\",\"芳苑\",\"Fangyuan\",\"芳苑\",\"芳苑\",\"芳苑\",\"芳苑\",\"芳苑\"\n\"C2G980\",\"田頭村\",\"Toutian Village\",\"田頭村\",\"田頭村\",\"田頭村\",\"田頭村\",\"田头村\"\n\"C2G9A0\",\"畜試所彰化\",\"Changhua Branch Of TLRI\",\"畜試所彰化\",\"畜試所彰化\",\"畜試所彰化\",\"畜試所彰化\",\"蓄试所彰化\"\n\"C2H950\",\"中寮\",\"Zhongliao\",\"中寮\",\"中寮\",\"中寮\",\"中寮\",\"中寮\"\n\"C2H9D0\",\"三隻寮\",\"Sanziliao\",\"三隻寮\",\"三隻寮\",\"三隻寮\",\"三隻寮\",\"三只寮\"\n\"C2H9E0\",\"國姓南港\",\"Guoxing Nangang\",\"国姓南港\",\"國姓南港\",\"國姓南港\",\"國姓南港\",\"国姓南港\"\n\"C2H9F0\",\"柑林\",\"Ganlin\",\"柑林\",\"柑林\",\"柑林\",\"柑林\",\"柑林\"\n\"C2H9G0\",\"百勝\",\"Bosheng\",\"百勝\",\"百勝\",\"百勝\",\"百勝\",\"百胜\"\n\"C2H9H0\",\"苗改南投蜂場\",\"Nantou Bee Farm Of Miaoli TARI\",\"苗改南投蜂場\",\"苗改南投蜂場\",\"苗改南投蜂場\",\"苗改南投蜂場\",\"苗改南投蜂场\"\n\"C2H9J0\",\"中台\",\"Zhongtai\",\"中台\",\"中台\",\"中台\",\"中台\",\"中台\"\n\"C2H9L0\",\"馬烈霸\",\"Malieba\",\"馬烈霸\",\"馬烈霸\",\"馬烈霸\",\"馬烈霸\",\"马烈霸\"\n\"C2H9M0\",\"發祥\",\"Faxiang\",\"發祥\",\"發祥\",\"發祥\",\"發祥\",\"发祥\"\n\"C2H9N0\",\"仁愛東眼\",\"Renaidong\",\"仁愛東眼\",\"仁愛東眼\",\"仁愛東眼\",\"仁愛東眼\",\"仁爱东眼\"\n\"C2H9P0\",\"伊拿谷\",\"Yinagu\",\"伊拿谷\",\"伊拿谷\",\"伊拿谷\",\"伊拿谷\",\"伊拿谷\"\n\"C2H9Q0\",\"北東眼山\",\"Beidongyanshan\",\"北東眼山\",\"北東眼山\",\"北東眼山\",\"北東眼山\",\"北东眼山\"\n\"C2H9R0\",\"卓社\",\"Zhuoshe\",\"卓社\",\"卓社\",\"卓社\",\"卓社\",\"卓社\"\n\"C2H9S0\",\"龍南\",\"Longnan\",\"龍南\",\"龍南\",\"龍南\",\"龍南\",\"龙南\"\n\"C2H9T0\",\"名間竹圍\",\"Mingjianzhuwei\",\"名間竹圍\",\"名間竹圍\",\"名間竹圍\",\"名間竹圍\",\"名间竹围\"\n\"C2H9U0\",\"鳳鵬\",\"Fengpeng\",\"鳳鵬\",\"鳳鵬\",\"鳳鵬\",\"鳳鵬\",\"凤鹏\"\n\"C2H9W0\",\"大坪頂農水\",\"Dapingding Station\",\"大坪頂農水\",\"大坪頂農水\",\"大坪頂農水\",\"大坪頂農水\",\"大坪顶农水\"\n\"C2I090\",\"鳳凰\",\"Fenghuang\",\"鳳凰\",\"鳳凰\",\"鳳凰\",\"鳳凰\",\"凤凰\"\n\"C2K240\",\"草嶺\",\"Caoling\",\"草嶺\",\"草嶺\",\"草嶺\",\"草嶺\",\"草岭\"\n\"C2K610\",\"草嶺石壁\",\"Caolingshibi\",\"草嶺石壁\",\"草嶺石壁\",\"草嶺石壁\",\"草嶺石壁\",\"草岭石壁\"\n\"C2K620\",\"馬光農場\",\"Maguang Organic Agriculture Circular Park\",\"馬光農場\",\"馬光農場\",\"馬光農場\",\"馬光農場\",\"马光农场\"\n\"C2K630\",\"荷苞\",\"Hebao\",\"荷苞\",\"荷苞\",\"荷苞\",\"荷苞\",\"荷苞\"\n\"C2M410\",\"馬頭山\",\"Matoushan\",\"馬頭山\",\"馬頭山\",\"馬頭山\",\"馬頭山\",\"马头山\"\n\"C2M620\",\"瑞里\",\"Ruili\",\"瑞里\",\"瑞里\",\"瑞里\",\"瑞里\",\"瑞里\"\n\"C2M910\",\"嘉義大學\",\"Chiayi University\",\"嘉義大学\",\"嘉義大學\",\"嘉義大學\",\"嘉義大學\",\"嘉义大学\"\n\"C2M920\",\"朴子農改\",\"Pozi DARES\",\"朴子農改\",\"朴子農改\",\"朴子農改\",\"朴子農改\",\"朴子农改\"\n\"C2M930\",\"石卓\",\"Shizhuo\",\"石卓\",\"石卓\",\"石卓\",\"石卓\",\"石卓\"\n\"C2M940\",\"日野賀\",\"Riyehe\",\"日野賀\",\"日野賀\",\"日野賀\",\"日野賀\",\"日野贺\"\n\"C2M950\",\"太和\",\"Taihe\",\"太和\",\"太和\",\"太和\",\"太和\",\"太和\"\n\"C2M960\",\"外寮\",\"Wailiao\",\"外寮\",\"外寮\",\"外寮\",\"外寮\",\"外寮\"\n\"C2M970\",\"碧湖\",\"Bihu\",\"碧湖\",\"碧湖\",\"碧湖\",\"碧湖\",\"碧湖\"\n\"C2N160\",\"西拉雅風管處\",\"Siraya NSAH\",\"西拉雅風管處\",\"西拉雅風管處\",\"西拉雅風管處\",\"西拉雅風管處\",\"西拉雅风管处\"\n\"C2O810\",\"曾文\",\"Cengwen\",\"曽文\",\"曾文\",\"曾文\",\"曾文\",\"曾文\"\n\"C2O930\",\"玉井\",\"Yujing\",\"玉井\",\"玉井\",\"玉井\",\"玉井\",\"玉井\"\n\"C2O950\",\"安南\",\"Annan\",\"安南\",\"安南\",\"安南\",\"安南\",\"安南\"\n\"C2R170\",\"屏東\",\"Pingdong\",\"屏東\",\"핑둥\",\"屏東\",\"屏東\",\"屏东\"\n\"C2R970\",\"屏科大\",\"National Pingtung University\",\"屏科大\",\"屏科大\",\"屏科大\",\"屏科大\",\"屏科大\"\n\"C2V250\",\"甲仙\",\"Jiaxian\",\"甲仙\",\"甲仙\",\"甲仙\",\"甲仙\",\"甲仙\"\n\"C2V260\",\"月眉\",\"Yuemei\",\"月眉\",\"月眉\",\"月眉\",\"月眉\",\"月眉\"\n\"C2V310\",\"美濃\",\"Meinong\",\"美濃\",\"美濃\",\"美濃\",\"美濃\",\"美浓\"\n\"C2W030\",\"金門農試所\",\"Kimmann\",\"金門農試所\",\"金門農試所\",\"金門農試所\",\"金門農試所\",\"金门农试所\"\n\"C2W230\",\"畜試所澎湖\",\"Penghu Field Area Of TLRI\",\"畜試所澎湖\",\"畜試所澎湖\",\"畜試所澎湖\",\"畜試所澎湖\",\"蓄试所澎湖\"\n\"CAG100\",\"王功漁港\",\"Wanggong Fishing Port\",\"王功漁港\",\"王功漁港\",\"王功漁港\",\"王功漁港\",\"王功渔港\"\n\"CAH030\",\"茶改場竹圍站\",\"TRES Chuwei Station\",\"茶改場竹圍站\",\"茶改場竹圍站\",\"茶改場竹圍站\",\"茶改場竹圍站\",\"茶改场竹围站\"\n\"CAJ050\",\"海口故事園區\",\"Haikou Story Camping Park\",\"海口故事園區\",\"海口故事園區\",\"海口故事園區\",\"海口故事園區\",\"海口故事园区\"\n\"CAL110\",\"布袋國中\",\"Budai Junior High School\",\"布袋中学校\",\"布袋國中\",\"布袋國中\",\"布袋國中\",\"布袋国中\"\n\"CAN130\",\"水試所海水繁養殖中心\",\"Mariculture Research Center\",\"水試所海水繁養殖中心\",\"水試所海水繁養殖中心\",\"水試所海水繁養殖中心\",\"水試所海水繁養殖中心\",\"水试所海水繁养殖中心\"\n\"CAN140\",\"六官養殖協會\",\"Liuguan Aquaculture\",\"六官養殖協會\",\"六官養殖協會\",\"六官養殖協會\",\"六官養殖協會\",\"六官养殖协会\"\n\"CAQ030\",\"崎峰國小\",\"Cifong Elementary School\",\"崎峰國小\",\"崎峰國小\",\"崎峰國小\",\"崎峰國小\",\"崎峰国小\"\n\"E2H360\",\"蓮華池\",\"Lienhuchih\",\"蓮華池\",\"蓮華池\",\"蓮華池\",\"蓮華池\",\"莲华池\"\n\"E2HA20\",\"林試畢祿溪站\",\"Pilushi\",\"林試畢祿溪站\",\"林試畢祿溪站\",\"林試畢祿溪站\",\"林試畢祿溪站\",\"林试毕禄溪站\"\n\"E2K600\",\"四湖植物園\",\"Sihu Botanical Garden\",\"四湖植物園\",\"四湖植物園\",\"四湖植物園\",\"四湖植物園\",\"四湖植物园\"\n\"E2P980\",\"林試六龜中心\",\"Lioukuei Research Center \",\"林試六龜中心\",\"林試六龜中心\",\"林試六龜中心\",\"林試六龜中心\",\"林试六龟中心\"\n\"E2P990\",\"林試扇平站\",\"Shanping\",\"林試扇平站\",\"林試扇平站\",\"林試扇平站\",\"林試扇平站\",\"林试扇平站\"\n\"E2S960\",\"林試太麻里2\",\"Taimalee2\",\"林試太麻里2\",\"林試太麻里2\",\"林試太麻里2\",\"林試太麻里2\",\"林试太麻里2\"\n\"E2S980\",\"林試太麻里1\",\"Taimalee Research Center 1\",\"林試太麻里1\",\"林試太麻里1\",\"林試太麻里1\",\"林試太麻里1\",\"林试太麻里1\"\n\"G2AI50\",\"關渡\",\"Guandu\",\"關渡\",\"關渡\",\"關渡\",\"關渡\",\"关渡\"\n\"G2F820\",\"農試所(霧峰)\",\"Taichung\",\"農試所(霧峰)\",\"農試所(霧峰)\",\"農試所(霧峰)\",\"農試所(霧峰)\",\"农试所(雾峰)\"\n\"G2L020\",\"農試嘉義分所\",\"Chiayi Sub-station Of TARI\",\"農試嘉義分所\",\"農試嘉義分所\",\"農試嘉義分所\",\"農試嘉義分所\",\"农试嘉义分所\"\n\"G2M350\",\"農試溪口農場\",\"Xikou Farm Of TARI\",\"農試溪口農場\",\"農試溪口農場\",\"農試溪口農場\",\"農試溪口農場\",\"农试溪口农场\"\n\"G2P820\",\"農試鳳山分所\",\"Fengshan Tropical Horticultural Of TARI\",\"農試鳳山分所\",\"農試鳳山分所\",\"農試鳳山分所\",\"農試鳳山分所\",\"农试凤山分所\"\n\"K2E360\",\"苗栗農改場\",\"Miaoli Agricultural Research Center\",\"苗栗農改場\",\"苗栗農改場\",\"苗栗農改場\",\"苗栗農改場\",\"苗栗农改场\"\n\"K2E710\",\"苗改生物防治研究中心\",\"Biological Control Branch Of Miaoli TARI\",\"苗改生物防治研究中心\",\"苗改生物防治研究中心\",\"苗改生物防治研究中心\",\"苗改生物防治研究中心\",\"苗改生物防治研究中心\"\n\"K2F750\",\"種苗改良場\",\"Shin She\",\"種苗改良場\",\"種苗改良場\",\"種苗改良場\",\"種苗改良場\",\"种苗改良场\"\n\"U2H480\",\"溪頭\",\"Hsi Tou\",\"溪頭\",\"溪頭\",\"溪頭\",\"溪頭\",\"溪头\"\n\"U2HA30\",\"臺大和社\",\"NTU Exfohoshe\",\"臺大和社\",\"臺大和社\",\"臺大和社\",\"臺大和社\",\"台大和社\"\n\"U2HA40\",\"臺大內茅埔\",\"NTU Exfoneimoupu\",\"臺大內茅埔\",\"臺大內茅埔\",\"臺大內茅埔\",\"臺大內茅埔\",\"台大內茅埔\"\n\"U2HA50\",\"臺大竹山\",\"NTU Experimental Forest\",\"臺大竹山\",\"臺大竹山\",\"臺大竹山\",\"臺大竹山\",\"台大竹山\"\n\"V2C250\",\"八德合作社\",\"Bade Cooperative\",\"八德合作社\",\"八德合作社\",\"八德合作社\",\"八德合作社\",\"八德合作社\"\n\"V2C260\",\"八德蔬果\",\"Bade Fruit And Vegetable\",\"八德蔬果\",\"八德蔬果\",\"八德蔬果\",\"八德蔬果\",\"八德蔬果\"\n\"V2K610\",\"大庄合作社\",\"Dazhuang Cooperative\",\"大庄合作社\",\"大庄合作社\",\"大庄合作社\",\"大庄合作社\",\"大庄合作社\"\n\"V2K620\",\"麥寮合作社\",\"Mailiao Cooperative\",\"麥寮合作社\",\"麥寮合作社\",\"麥寮合作社\",\"麥寮合作社\",\"麦寮合作社\"\n\"C0A520\",\"山佳\",\"Shanjia\",\"山佳\",\"山佳\",\"山佳\",\"山佳\",\"山佳\"\n\"C0A530\",\"坪林\",\"Pinglin\",\"坪林\",\"坪林\",\"坪林\",\"坪林\",\"坪林\"\n\"C0A550\",\"泰平\",\"Taiping\",\"泰平\",\"泰平\",\"泰平\",\"泰平\",\"泰平\"\n\"C0A570\",\"桶後\",\"Tonghou\",\"桶後\",\"桶後\",\"桶後\",\"桶後\",\"桶后\"\n\"C0A640\",\"石碇\",\"Shihding\",\"石碇\",\"石碇\",\"石碇\",\"石碇\",\"石碇\"\n\"C0A770\",\"科教館\",\"Science Education Center \",\"科教館\",\"科教館\",\"科教館\",\"科教館\",\"科教馆\"\n\"C0A860\",\"大坪\",\"Daping\",\"大坪\",\"大坪\",\"大坪\",\"大坪\",\"大坪\"\n\"C0A870\",\"五指山\",\"Wujhihshan\",\"五指山\",\"五指山\",\"五指山\",\"五指山\",\"五指山\"\n\"C0A890\",\"雙溪\",\"Shuangsi\",\"双渓\",\"雙溪\",\"雙溪\",\"雙溪\",\"双溪\"\n\"C0A931\",\"三和\",\"Sanhe\",\"三和\",\"三和\",\"三和\",\"三和\",\"三和\"\n\"C0A940\",\"金山\",\"Jinshan\",\"金山\",\"金山\",\"金山\",\"金山\",\"金山\"\n\"C0A950\",\"鼻頭角\",\"Bitoujiao\",\"鼻頭角\",\"鼻頭角\",\"鼻頭角\",\"鼻頭角\",\"鼻头角\"\n\"C0A970\",\"三貂角\",\"Sandiaojiao\",\"三貂角\",\"三貂角\",\"三貂角\",\"三貂角\",\"三貂角\"\n\"C0A980\",\"社子\",\"Shezih\",\"社子\",\"社子\",\"社子\",\"社子\",\"社子\"\n\"C0A9C0\",\"天母\",\"Tianmu\",\"天母\",\"天母\",\"天母\",\"天母\",\"天母\"\n\"C0A9F0\",\"內湖\",\"Neihu\",\"内湖\",\"內湖\",\"內湖\",\"內湖\",\"内湖\"\n\"C0AC40\",\"大屯山\",\"Datunshan\",\"大屯山\",\"大屯山\",\"大屯山\",\"大屯山\",\"大屯山\"\n\"C0AC60\",\"三峽\",\"Sanshia\",\"三峡\",\"三峽\",\"三峽\",\"三峽\",\"三峡\"\n\"C0AC70\",\"信義\",\"Xinyi\",\"信義\",\"信義\",\"信義\",\"信義\",\"信义\"\n\"C0AC80\",\"文山\",\"Wenshan\",\"文山\",\"文山\",\"文山\",\"文山\",\"文山\"\n\"C0ACA0\",\"新莊\",\"Xinzhuang\",\"新荘\",\"新莊\",\"新莊\",\"新莊\",\"新庄\"\n\"C0AD10\",\"八里\",\"Bali\",\"八里\",\"八里\",\"八里\",\"八里\",\"八里\"\n\"C0AD30\",\"蘆洲\",\"Lujhou\",\"蘆洲\",\"蘆洲\",\"蘆洲\",\"蘆洲\",\"芦洲\"\n\"C0AD40\",\"土城\",\"Tucheng\",\"土城\",\"土城\",\"土城\",\"土城\",\"土城\"\n\"C0AD50\",\"鶯歌\",\"Yingge\",\"鶯歌\",\"鶯歌\",\"鶯歌\",\"鶯歌\",\"莺歌\"\n\"C0AG80\",\"中和\",\"Zhonghe\",\"中和\",\"中和\",\"中和\",\"中和\",\"中和\"\n\"C0AH00\",\"汐止\",\"Xizhi\",\"汐止\",\"汐止\",\"汐止\",\"汐止\",\"汐止\"\n\"C0AH10\",\"永和\",\"Yonghe\",\"永和\",\"永和\",\"永和\",\"永和\",\"永和\"\n\"C0AH30\",\"五分山\",\"Wufengshan\",\"五分山\",\"五分山\",\"五分山\",\"五分山\",\"五分山\"\n\"C0AH40\",\"平等\",\"Pingdeng\",\"平等\",\"平等\",\"平等\",\"平等\",\"平等\"\n\"C0AH50\",\"林口\",\"Linkou\",\"林口\",\"林口\",\"林口\",\"林口\",\"林口\"\n\"C0AH70\",\"松山\",\"Songshan\",\"松山\",\"松山\",\"松山\",\"松山\",\"松山\"\n\"C0AH80\",\"深坑\",\"Shenkeng\",\"深坑\",\"深坑\",\"深坑\",\"深坑\",\"深坑\"\n\"C0AH90\",\"福山植物園\",\"Fushan Botanical Garden\",\"福山植物園\",\"福山植物園\",\"福山植物園\",\"福山植物園\",\"福山植物园\"\n\"C0AI00\",\"五股\",\"Wugu\",\"五股\",\"五股\",\"五股\",\"五股\",\"五股\"\n\"C0AI10\",\"屈尺\",\"Quchi\",\"屈尺\",\"屈尺\",\"屈尺\",\"屈尺\",\"屈尺\"\n\"C0AI20\",\"白沙灣\",\"Baishawan\",\"白沙灣\",\"白沙灣\",\"白沙灣\",\"白沙灣\",\"白沙湾\"\n\"C0AI30\",\"三重\",\"Sanchong\",\"三重\",\"三重\",\"三重\",\"三重\",\"三重\"\n\"C0AI40\",\"石牌\",\"Shipai\",\"石牌\",\"石牌\",\"石牌\",\"石牌\",\"石牌\"\n\"C0AJ20\",\"野柳\",\"Yehliou\",\"野柳\",\"野柳\",\"野柳\",\"野柳\",\"野柳\"\n\"C0AJ30\",\"淡水觀海\",\"Danshuei Guanhai\",\"淡水觀海\",\"淡水觀海\",\"淡水觀海\",\"淡水觀海\",\"淡水观海\"\n\"C0AJ40\",\"石門\",\"Shimen\",\"石門\",\"石門\",\"石門\",\"石門\",\"石门\"\n\"C0AJ50\",\"水湳洞\",\"Shuinandong\",\"水湳洞\",\"水湳洞\",\"水湳洞\",\"水湳洞\",\"水湳洞\"\n\"C0AJ60\",\"六塊厝\",\"Lioukuaitsuo\",\"六塊厝\",\"六塊厝\",\"六塊厝\",\"六塊厝\",\"六块厝\"\n\"C0AJ70\",\"田寮\",\"Tianliao\",\"田寮\",\"田寮\",\"田寮\",\"田寮\",\"田寮\"\n\"C0AJ80\",\"板橋\",\"Banchiao\",\"板橋\",\"板橋\",\"板橋\",\"板橋\",\"板桥\"\n\"C0AJ90\",\"澳底\",\"Aodi\",\"澳底\",\"澳底\",\"澳底\",\"澳底\",\"澳底\"\n\"C0AK10\",\"太平里\",\"Taiping Vil.\",\"太平里\",\"太平里\",\"太平里\",\"太平里\",\"太平里\"\n\"C0AK30\",\"硬漢嶺\",\"Yinghanling\",\"硬漢嶺\",\"硬漢嶺\",\"硬漢嶺\",\"硬漢嶺\",\"硬汉岭\"\n\"C0B010\",\"七堵\",\"Qidu\",\"七堵\",\"七堵\",\"七堵\",\"七堵\",\"七堵\"\n\"C0B020\",\"基隆嶼\",\"Keelung Islet\",\"基隆嶼\",\"基隆嶼\",\"基隆嶼\",\"基隆嶼\",\"基隆屿\"\n\"C0B040\",\"大武崙\",\"Dawulun\",\"大武崙\",\"大武崙\",\"大武崙\",\"大武崙\",\"大武仑\n\"\n\"C0B050\",\"八斗子\",\"Badouzi\",\"八斗子\",\"八斗子\",\"八斗子\",\"八斗子\",\"八斗子\"\n\"C0B060\",\"暖暖\",\"Nuannuan\",\"暖暖\",\"暖暖\",\"暖暖\",\"暖暖\",\"暖暖\"\n\"C0C460\",\"復興\",\"Fuxing\",\"復興\",\"復興\",\"復興\",\"復興\",\"复兴\"\n\"C0C480\",\"桃園\",\"Taoyuan\",\"桃園\",\"타오위안\",\"桃園\",\"桃園\",\"桃园\"\n\"C0C490\",\"八德\",\"Bade\",\"八徳\",\"八德\",\"八德\",\"八德\",\"八德\"\n\"C0C590\",\"觀音\",\"Guanyin\",\"観音\",\"觀音\",\"觀音\",\"觀音\",\"观音\"\n\"C0C620\",\"蘆竹\",\"Luzhu\",\"蘆竹\",\"蘆竹\",\"蘆竹\",\"蘆竹\",\"芦竹\"\n\"C0C630\",\"大溪\",\"Dasi\",\"大渓\",\"大溪\",\"大溪\",\"大溪\",\"大溪\"\n\"C0C650\",\"平鎮\",\"Pingjhen\",\"平鎮\",\"平鎮\",\"平鎮\",\"平鎮\",\"平镇\"\n\"C0C660\",\"楊梅\",\"Yangmei\",\"楊梅\",\"楊梅\",\"楊梅\",\"楊梅\",\"杨梅\"\n\"C0C670\",\"龍潭\",\"Longtan\",\"龍潭\",\"龍潭\",\"龍潭\",\"龍潭\",\"龙潭\"\n\"C0C680\",\"龜山\",\"Guishan\",\"亀山\",\"龜山\",\"龜山\",\"龜山\",\"龟山\"\n\"C0C700\",\"中壢\",\"Zhongli\",\"中壢\",\"中壢\",\"中壢\",\"中壢\",\"中坜\"\n\"C0C710\",\"大溪永福\",\"Yongfu Daxi\",\"大溪永福\",\"大溪永福\",\"大溪永福\",\"大溪永福\",\"大溪永福\"\n\"C0C720\",\"竹圍\",\"Jhuwei\",\"竹圍\",\"竹圍\",\"竹圍\",\"竹圍\",\"竹围\"\n\"C0C730\",\"中大臨海站\",\"Jhongda Coastal Station\",\"中大臨海站\",\"中大臨海站\",\"中大臨海站\",\"中大臨海站\",\"中大临海站\"\n\"C0C740\",\"觀音工業區\",\"Guanyin Industrial Area\",\"觀音工業區\",\"觀音工業區\",\"觀音工業區\",\"觀音工業區\",\"观音工业区\"\n\"C0C750\",\"新興坑尾\",\"Sinsingkengwei\",\"新興坑尾\",\"新興坑尾\",\"新興坑尾\",\"新興坑尾\",\"新兴坑尾\"\n\"C0C790\",\"東眼山\",\"Dongyanshan\",\"東眼山\",\"東眼山\",\"東眼山\",\"東眼山\",\"东眼山\"\n\"C0C800\",\"四稜\",\"Sileng\",\"四稜\",\"四稜\",\"四稜\",\"四稜\",\"四棱\"\n\"C0D360\",\"梅花\",\"Meihua\",\"梅花\",\"梅花\",\"梅花\",\"梅花\",\"梅花\"\n\"C0D430\",\"峨眉\",\"Emei\",\"峨眉\",\"峨眉\",\"峨眉\",\"峨眉\",\"峨眉\"\n\"C0D480\",\"打鐵坑\",\"Datiekeng\",\"打鐵坑\",\"打鐵坑\",\"打鐵坑\",\"打鐵坑\",\"打铁坑\"\n\"C0D540\",\"橫山\",\"Hengshan\",\"横山\",\"橫山\",\"橫山\",\"橫山\",\"横山\"\n\"C0D550\",\"雪霸\",\"Xueba\",\"雪霸\",\"雪霸\",\"雪霸\",\"雪霸\",\"雪霸\"\n\"C0D560\",\"竹東\",\"Zhudong\",\"竹東\",\"竹東\",\"竹東\",\"竹東\",\"竹东\"\n\"C0D580\",\"寶山\",\"Baoshan\",\"宝山\",\"寶山\",\"寶山\",\"寶山\",\"宝山\"\n\"C0D590\",\"新豐\",\"Sinfong\",\"新豊\",\"新豐\",\"新豐\",\"新豐\",\"新丰\"\n\"C0D650\",\"湖口\",\"Hukou\",\"湖口\",\"湖口\",\"湖口\",\"湖口\",\"湖口\"\n\"C0D660\",\"新竹市東區\",\"Dongqu Hsinshu City\",\"新竹市東區\",\"新竹市東區\",\"新竹市東區\",\"新竹市東區\",\"新竹市东区\"\n\"C0D670\",\"海天一線\",\"Haitianyisian\",\"海天一線\",\"海天一線\",\"海天一線\",\"海天一線\",\"海天一线\"\n\"C0D680\",\"香山濕地\",\"Siangshan Wetland\",\"香山濕地\",\"香山濕地\",\"香山濕地\",\"香山濕地\",\"香山湿地\"\n\"C0D690\",\"外湖\",\"Waihu\",\"外湖\",\"外湖\",\"外湖\",\"外湖\",\"外湖\"\n\"C0D700\",\"關西\",\"Guanxi\",\"関西\",\"關西\",\"關西\",\"關西\",\"关西\"\n\"C0D750\",\"樂山林道6k\",\"Leshan 6k\",\"樂山林道6k\",\"樂山林道6k\",\"樂山林道6k\",\"樂山林道6k\",\"乐林山道6K\"\n\"C0D760\",\"大坪苗圃\",\"Daping Nurserygarden\",\"大坪苗圃\",\"大坪苗圃\",\"大坪苗圃\",\"大坪苗圃\",\"大坪苗圃\"\n\"C0E420\",\"竹南\",\"Jhunan\",\"竹南\",\"竹南\",\"竹南\",\"竹南\",\"竹南\"\n\"C0E430\",\"南庄\",\"Nanzhuang\",\"南庄\",\"南庄\",\"南庄\",\"南庄\",\"南庄\"\n\"C0E550\",\"明德\",\"Mingde\",\"明德\",\"明德\",\"明德\",\"明德\",\"明德\"\n\"C0E570\",\"白沙屯\",\"Baishatun\",\"白沙屯\",\"白沙屯\",\"白沙屯\",\"白沙屯\",\"白沙屯\"\n\"C0E590\",\"通霄\",\"Tongxiao\",\"通霄\",\"通霄\",\"通霄\",\"通霄\",\"通宵\"\n\"C0E610\",\"馬都安\",\"Madu-An\",\"馬都安\",\"馬都安\",\"馬都安\",\"馬都安\",\"马都安\"\n\"C0E730\",\"頭份\",\"Toufen\",\"頭份\",\"頭份\",\"頭份\",\"頭份\",\"头份\"\n\"C0E740\",\"造橋\",\"Zaoqiao\",\"造橋\",\"造橋\",\"造橋\",\"造橋\",\"造桥\"\n\"C0E750\",\"苗栗\",\"Miaoli\",\"苗栗\",\"먀오리\",\"苗栗\",\"苗栗\",\"苗栗\"\n\"C0E780\",\"銅鑼\",\"Tongluo\",\"銅鑼\",\"銅鑼\",\"銅鑼\",\"銅鑼\",\"铜锣\"\n\"C0E791\",\"卓蘭\",\"Zhuolan\",\"卓蘭\",\"卓蘭\",\"卓蘭\",\"卓蘭\",\"卓兰\"\n\"C0E810\",\"西湖\",\"Xihu\",\"西湖\",\"西湖\",\"西湖\",\"西湖\",\"西湖\"\n\"C0E820\",\"獅潭\",\"Shitan\",\"獅潭\",\"獅潭\",\"獅潭\",\"獅潭\",\"狮潭\"\n\"C0E830\",\"苑裡\",\"Yuanli\",\"苑裡\",\"苑裡\",\"苑裡\",\"苑裡\",\"苑里\"\n\"C0E850\",\"大河\",\"Dahe\",\"大河\",\"大河\",\"大河\",\"大河\",\"大河\"\n\"C0E870\",\"高鐵苗栗\",\"THSR Miaoli\",\"高鐵苗栗\",\"高鐵苗栗\",\"高鐵苗栗\",\"高鐵苗栗\",\"高铁苗栗\"\n\"C0E910\",\"海埔\",\"Haipu\",\"海埔\",\"海埔\",\"海埔\",\"海埔\",\"海埔\"\n\"C0E920\",\"通霄漁港\",\"Tongsiao Fishing Harbor\",\"通霄漁港\",\"通霄漁港\",\"通霄漁港\",\"通霄漁港\",\"通宵渔港\"\n\"C0E930\",\"龍鳳\",\"Longfong\",\"龍鳳\",\"龍鳳\",\"龍鳳\",\"龍鳳\",\"龙凤\"\n\"C0E940\",\"雪見\",\"Shiuejian\",\"雪見\",\"雪見\",\"雪見\",\"雪見\",\"雪见\"\n\"C0E950\",\"松安\",\"Songan\",\"松安\",\"松安\",\"松安\",\"松安\",\"松安\"\n\"C0E960\",\"觀霧分站\",\"Guanwu\",\"觀霧分站\",\"觀霧分站\",\"觀霧分站\",\"觀霧分站\",\"观雾分站\"\n\"C0F0A0\",\"雪山圈谷\",\"Xueshanjuangu\",\"雪山圈谷\",\"雪山圈谷\",\"雪山圈谷\",\"雪山圈谷\",\"雪山圈谷\"\n\"C0F0B0\",\"石岡\",\"Shigang\",\"石岡\",\"스강\",\"石岡\",\"石岡\",\"石冈\"\n\"C0F0C0\",\"中坑\",\"Zhongkeng\",\"中坑\",\"中坑\",\"中坑\",\"中坑\",\"中坑\"\n\"C0F0D0\",\"審馬陣\",\"Shenmazhen\",\"審馬陣\",\"審馬陣\",\"審馬陣\",\"審馬陣\",\"审马阵\"\n\"C0F0E0\",\"南湖圈谷\",\"Nanhuquangu\",\"南湖圈谷\",\"南湖圈谷\",\"南湖圈谷\",\"南湖圈谷\",\"南胡圈谷\"\n\"C0F850\",\"東勢\",\"Dongshi\",\"東勢\",\"둥스\",\"東勢\",\"東勢\",\"东势\"\n\"C0F970\",\"大坑\",\"Dakeng\",\"大坑\",\"大坑\",\"大坑\",\"大坑\",\"大坑\"\n\"C0F9I0\",\"神岡\",\"Shengang\",\"神岡\",\"룽징\",\"神岡\",\"神岡\",\"神冈\"\n\"C0F9K0\",\"大安\",\"Da-An\",\"大安\",\"다안\",\"大安\",\"大安\",\"大安\"\n\"C0F9L0\",\"后里\",\"Houli\",\"后里\",\"허우리\",\"后里\",\"后里\",\"后里\"\n\"C0F9M0\",\"豐原\",\"Fengyuan\",\"豊原\",\"펑위안\",\"豐原\",\"豐原\",\"丰原\"\n\"C0F9N0\",\"大里\",\"Dali\",\"大里\",\"다리\",\"大里\",\"大里\",\"大里\"\n\"C0F9O0\",\"潭子\",\"Tanzi\",\"潭子\",\"탄쯔\",\"潭子\",\"潭子\",\"潭子\"\n\"C0F9P0\",\"清水\",\"Qingshui\",\"清水\",\"清水\",\"清水\",\"清水\",\"清水\"\n\"C0F9Q0\",\"外埔\",\"Waipu\",\"外埔\",\"와이푸\",\"外埔\",\"外埔\",\"外埔\"\n\"C0F9R0\",\"龍井\",\"Longjing\",\"龍井\",\"룽징\",\"龍井\",\"龍井\",\"龙井\"\n\"C0F9S0\",\"烏日\",\"Wuri\",\"烏日\",\"우르\",\"烏日\",\"烏日\",\"乌日\"\n\"C0F9T0\",\"西屯\",\"Xitun\",\"西屯\",\"시툰\",\"西屯\",\"西屯\",\"西屯\"\n\"C0F9U0\",\"南屯\",\"Nantun\",\"南屯\",\"난툰\",\"南屯\",\"南屯\",\"南屯\"\n\"C0F9V0\",\"新社\",\"Xinshe\",\"新社\",\"신서\",\"新社\",\"新社\",\"新社\"\n\"C0F9X0\",\"大雅(中科園區)\",\"Daya\",\"大雅(中科園區)\",\"大雅(中科園區)\",\"大雅(中科園區)\",\"大雅(中科園區)\",\"大雅(中科园区)\"\n\"C0F9Y0\",\"桃山\",\"Taoshan\",\"桃山\",\"桃山\",\"桃山\",\"桃山\",\"桃山\"\n\"C0F9Z0\",\"雪山東峰\",\"Xueshandongfeng\",\"雪山東峰\",\"雪山東峰\",\"雪山東峰\",\"雪山東峰\",\"雪山东峰\"\n\"C0FA10\",\"松柏\",\"Songbai\",\"松柏\",\"松柏\",\"松柏\",\"松柏\",\"松柏\"\n\"C0FA20\",\"溫寮\",\"Wunliao\",\"溫寮\",\"溫寮\",\"溫寮\",\"溫寮\",\"温寮\"\n\"C0FA30\",\"梧棲\",\"Wuqi\",\"梧棲\",\"우치\",\"梧棲\",\"梧棲\",\"梧栖\"\n\"C0FA40\",\"臺中電廠\",\"Taichung?Power Plant\",\"臺中電廠\",\"臺中電廠\",\"臺中電廠\",\"臺中電廠\",\"台中电厂\"\n\"C0FA50\",\"霧峰\",\"Wufeng\",\"霧峰\",\"우펑\",\"霧峰\",\"霧峰\",\"雾峰\"\n\"C0FA60\",\"鞍馬山工作站\",\"Anmashan Office\",\"鞍馬山工作站\",\"鞍馬山工作站\",\"鞍馬山工作站\",\"鞍馬山工作站\",\"鞍马山工作站\"\n\"C0FA70\",\"大雪山埡口\",\"Dashiueshan Yakou\",\"大雪山埡口\",\"大雪山埡口\",\"大雪山埡口\",\"大雪山埡口\",\"大雪山哑口\"\n\"C0FA80\",\"小雪山天池\",\"Shiaushiueshan Tianchr\",\"小雪山天池\",\"小雪山天池\",\"小雪山天池\",\"小雪山天池\",\"小雪山天池\"\n\"C0FA90\",\"小雪山林道\",\"Shiaushiueshan\",\"小雪山林道\",\"小雪山林道\",\"小雪山林道\",\"小雪山林道\",\"小雪山林道\"\n\"C0FB00\",\"大雪山\",\"Dashiueshan\",\"大雪山\",\"大雪山\",\"大雪山\",\"大雪山\",\"大雪山\"\n\"C0FB10\",\"出雲山苗圃\",\"Chuyunshan\",\"出雲山苗圃\",\"出雲山苗圃\",\"出雲山苗圃\",\"出雲山苗圃\",\"出云山苗圃\"\n\"C0FB20\",\"雪山橋\",\"Shiueshanchiau\",\"雪山橋\",\"雪山橋\",\"雪山橋\",\"雪山橋\",\"雪山桥\"\n\"C0FB30\",\"八仙山苗圃\",\"Bashianshan\",\"八仙山苗圃\",\"八仙山苗圃\",\"八仙山苗圃\",\"八仙山苗圃\",\"八仙山苗圃\"\n\"C0FB40\",\"中橫21.6k\",\"Chungheng 21.6k\",\"中橫21.6k\",\"中橫21.6k\",\"中橫21.6k\",\"中橫21.6k\",\"中橫21.6K\"\n\"C0FB70\",\"大甲溪埔\",\"Dajia, Xipu\",\"大甲溪埔\",\"大甲溪埔\",\"大甲溪埔\",\"大甲溪埔\",\"大甲溪埔\"\n\"C0G620\",\"芬園\",\"Fenyuan\",\"芬園\",\"芬園\",\"芬園\",\"芬園\",\"芬园\"\n\"C0G650\",\"員林\",\"Yuanlin\",\"員林\",\"員林\",\"員林\",\"員林\",\"员林\"\n\"C0G660\",\"溪湖\",\"Xihu\",\"渓湖\",\"溪湖\",\"溪湖\",\"溪湖\",\"溪湖\"\n\"C0G720\",\"溪州\",\"Xizhou\",\"渓州\",\"溪州\",\"溪州\",\"溪州\",\"溪洲\"\n\"C0G730\",\"二林\",\"Erlin\",\"二林\",\"二林\",\"二林\",\"二林\",\"二林\"\n\"C0G740\",\"大城\",\"Dacheng\",\"大城\",\"大城\",\"大城\",\"大城\",\"大城\"\n\"C0G770\",\"福興\",\"Fuxing\",\"福興\",\"福興\",\"福興\",\"福興\",\"福兴\"\n\"C0G780\",\"秀水\",\"Xiushui\",\"秀水\",\"秀水\",\"秀水\",\"秀水\",\"秀水\"\n\"C0G800\",\"埔鹽\",\"Puyan\",\"埔塩\",\"埔鹽\",\"埔鹽\",\"埔鹽\",\"埔盐\"\n\"C0G810\",\"埔心\",\"Puxin\",\"埔心\",\"埔心\",\"埔心\",\"埔心\",\"埔心\"\n\"C0G820\",\"田尾\",\"Tianwei\",\"田尾\",\"田尾\",\"田尾\",\"田尾\",\"田尾\"\n\"C0G830\",\"埤頭\",\"Pitou\",\"埤頭\",\"埤頭\",\"埤頭\",\"埤頭\",\"埤头\"\n\"C0G860\",\"社頭\",\"Shetou\",\"社頭\",\"社頭\",\"社頭\",\"社頭\",\"社头\"\n\"C0G880\",\"二水\",\"Ershui\",\"二水\",\"二水\",\"二水\",\"二水\",\"二水\"\n\"C0G890\",\"伸港\",\"Shenggang\",\"伸港\",\"伸港\",\"伸港\",\"伸港\",\"伸港\"\n\"C0G900\",\"線西\",\"Xianxi\",\"線西\",\"線西\",\"線西\",\"線西\",\"线西\"\n\"C0G910\",\"花壇\",\"Huatan\",\"花壇\",\"花壇\",\"花壇\",\"花壇\",\"花坛\"\n\"C0G920\",\"永靖\",\"Yongjing \",\"永靖\",\"永靖\",\"永靖\",\"永靖\",\"永靖\"\n\"C0G940\",\"竹塘\",\"Zhutang\",\"竹塘\",\"竹塘\",\"竹塘\",\"竹塘\",\"竹塘\"\n\"C0G950\",\"防潮門\",\"Fangchaomen\",\"防潮門\",\"防潮門\",\"防潮門\",\"防潮門\",\"防潮门\"\n\"C0G960\",\"福寶\",\"Fubao\",\"福寶\",\"福寶\",\"福寶\",\"福寶\",\"福宝\"\n\"C0G970\",\"三豐\",\"Sanfong\",\"三豐\",\"三豐\",\"三豐\",\"三豐\",\"三丰\"\n\"C0G9B0\",\"和美\",\"Hemei\",\"和美\",\"和美\",\"和美\",\"和美\",\"和美\"\n\"C0H890\",\"埔里\",\"Puli\",\"埔里\",\"埔里\",\"埔里\",\"埔里\",\"埔里\"\n\"C0H960\",\"草屯\",\"Caotun\",\"草屯\",\"草屯\",\"草屯\",\"草屯\",\"草屯\"\n\"C0H990\",\"昆陽\",\"Kunyang\",\"昆陽\",\"昆陽\",\"昆陽\",\"昆陽\",\"昆阳\"\n\"C0H9A0\",\"神木村\",\"Shenmu Village\",\"神木村\",\"神木村\",\"神木村\",\"神木村\",\"神木村\"\n\"C0H9C0\",\"合歡山\",\"Hehuan Mountain\",\"合歡山\",\"合歡山\",\"合歡山\",\"合歡山\",\"合欢山\"\n\"C0I010\",\"廬山\",\"Lushan\",\"廬山\",\"廬山\",\"廬山\",\"廬山\",\"庐山\"\n\"C0I080\",\"信義\",\"Xinyi\",\"信義\",\"信義\",\"信義\",\"信義\",\"信义\"\n\"C0I110\",\"竹山\",\"Zhushan\",\"竹山\",\"竹山\",\"竹山\",\"竹山\",\"竹山\"\n\"C0I360\",\"水里\",\"Shuili\",\"水里\",\"水里\",\"水里\",\"水里\",\"水里\"\n\"C0I370\",\"魚池\",\"Yuchi\",\"魚池\",\"魚池\",\"魚池\",\"魚池\",\"鱼池\"\n\"C0I380\",\"集集\",\"Jiji\",\"集集\",\"集集\",\"集集\",\"集集\",\"集集\"\n\"C0I390\",\"仁愛\",\"Ren'Ai\",\"仁愛\",\"仁愛\",\"仁愛\",\"仁愛\",\"仁爱\"\n\"C0I410\",\"名間\",\"Mingjian\",\"名間\",\"名間\",\"名間\",\"名間\",\"名间\"\n\"C0I420\",\"國姓\",\"Guoxing\",\"国姓\",\"國姓\",\"國姓\",\"國姓\",\"国姓\"\n\"C0I460\",\"南投\",\"Nantou\",\"南投\",\"난터우\",\"南投\",\"南投\",\"南投\"\n\"C0I480\",\"梅峰\",\"Meifeng\",\"梅峰\",\"梅峰\",\"梅峰\",\"梅峰\",\"梅峰\"\n\"C0I490\",\"萬大林道\",\"Wandalindao\",\"萬大林道\",\"萬大林道\",\"萬大林道\",\"萬大林道\",\"万大林道\"\n\"C0I520\",\"玉山風口\",\"Yushanfengkou\",\"玉山風口\",\"玉山風口\",\"玉山風口\",\"玉山風口\",\"玉山风口\"\n\"C0I530\",\"小奇萊\",\"Xiaoqilai\",\"小奇萊\",\"小奇萊\",\"小奇萊\",\"小奇萊\",\"小奇莱\"\n\"C0I540\",\"奇萊稜線\",\"Qilailengxian\",\"奇萊稜線\",\"奇萊稜線\",\"奇萊稜線\",\"奇萊稜線\",\"奇莱棱线\"\n\"C0K250\",\"崙背\",\"Lunbei\",\"崙背\",\"崙背\",\"崙背\",\"崙背\",\"仑背\"\n\"C0K280\",\"四湖\",\"Sihu\",\"四湖\",\"四湖\",\"四湖\",\"四湖\",\"四湖\"\n\"C0K291\",\"宜梧\",\"Yiwu\",\"宜梧\",\"宜梧\",\"宜梧\",\"宜梧\",\"宜梧\"\n\"C0K330\",\"虎尾\",\"Huwei\",\"虎尾\",\"虎尾\",\"虎尾\",\"虎尾\",\"虎尾\"\n\"C0K390\",\"土庫\",\"Tuku\",\"土庫\",\"土庫\",\"土庫\",\"土庫\",\"土库\"\n\"C0K400\",\"斗六\",\"Douliu\",\"斗六\",\"斗六\",\"斗六\",\"斗六\",\"斗六\"\n\"C0K410\",\"北港\",\"Beigang\",\"北港\",\"北港\",\"北港\",\"北港\",\"北港\"\n\"C0K420\",\"西螺\",\"Xiluo\",\"西螺\",\"西螺\",\"西螺\",\"西螺\",\"西螺\"\n\"C0K430\",\"褒忠\",\"Baozhong\",\"褒忠\",\"褒忠\",\"褒忠\",\"褒忠\",\"褒忠\"\n\"C0K440\",\"二崙\",\"Erlun\",\"二崙\",\"二崙\",\"二崙\",\"二崙\",\"二仑\"\n\"C0K450\",\"大埤\",\"Dapi\",\"大埤\",\"大埤\",\"大埤\",\"大埤\",\"大埤\"\n\"C0K460\",\"斗南\",\"Dounan\",\"斗南\",\"斗南\",\"斗南\",\"斗南\",\"鬥南\"\n\"C0K470\",\"林內\",\"Linnei\",\"林内\",\"林內\",\"林內\",\"林內\",\"林內\"\n\"C0K480\",\"莿桐\",\"Citong\",\"莿桐\",\"莿桐\",\"莿桐\",\"莿桐\",\"莿桐\"\n\"C0K500\",\"元長\",\"Yuanchang\",\"元長\",\"元長\",\"元長\",\"元長\",\"元长\"\n\"C0K510\",\"水林\",\"Shuilin\",\"水林\",\"水林\",\"水林\",\"水林\",\"水林\"\n\"C0K530\",\"臺西\",\"Taixi\",\"台西\",\"臺西\",\"臺西\",\"臺西\",\"台西\"\n\"C0K550\",\"蔦松\",\"Niaosong\",\"蔦松\",\"蔦松\",\"蔦松\",\"蔦松\",\"茑松\"\n\"C0K560\",\"棋山\",\"Qishan\",\"棋山\",\"棋山\",\"棋山\",\"棋山\",\"棋山\"\n\"C0K580\",\"高鐵雲林\",\"THSR Yunlin\",\"高鐵雲林\",\"高鐵雲林\",\"高鐵雲林\",\"高鐵雲林\",\"高铁云林\"\n\"C0K590\",\"雲林東勢\",\"Dongshi, Yunlin County\",\"雲林東勢\",\"雲林東勢\",\"雲林東勢\",\"雲林東勢\",\"云林东势\"\n\"C0K600\",\"箔子寮\",\"Bozihliao\",\"箔子寮\",\"箔子寮\",\"箔子寮\",\"箔子寮\",\"箔子寮\"\n\"C0M520\",\"東後寮\",\"Donghouliao\",\"東後寮\",\"東後寮\",\"東後寮\",\"東後寮\",\"东后寮\"\n\"C0M530\",\"奮起湖\",\"Fenqihu\",\"奮起湖\",\"奮起湖\",\"奮起湖\",\"奮起湖\",\"奋起湖\"\n\"C0M640\",\"中埔\",\"Zhongpu\",\"中埔\",\"中埔\",\"中埔\",\"中埔\",\"中埔\"\n\"C0M650\",\"朴子\",\"Puzi\",\"朴子\",\"朴子\",\"朴子\",\"朴子\",\"朴子\"\n\"C0M660\",\"溪口\",\"Xikou\",\"渓口\",\"溪口\",\"溪口\",\"溪口\",\"溪口\"\n\"C0M670\",\"大林\",\"Dalin\",\"大林\",\"大林\",\"大林\",\"大林\",\"大林\"\n\"C0M680\",\"太保\",\"Taibao\",\"太保\",\"太保\",\"太保\",\"太保\",\"太保\"\n\"C0M690\",\"水上\",\"Shuishang\",\"水上\",\"水上\",\"水上\",\"水上\",\"水上\"\n\"C0M700\",\"竹崎\",\"Zhuqi\",\"竹崎\",\"竹崎\",\"竹崎\",\"竹崎\",\"竹崎\"\n\"C0M710\",\"東石\",\"Dongshi\",\"東石\",\"東石\",\"東石\",\"東石\",\"东石\"\n\"C0M720\",\"番路\",\"Fanlu\",\"番路\",\"番路\",\"番路\",\"番路\",\"番路\"\n\"C0M730\",\"嘉義市東區\",\"Dongqu Chiayi City\",\"嘉義市東區\",\"嘉義市東區\",\"嘉義市東區\",\"嘉義市東區\",\"嘉义市东区\"\n\"C0M740\",\"六腳\",\"Liujiao\",\"六脚\",\"六腳\",\"六腳\",\"六腳\",\"六脚\"\n\"C0M750\",\"布袋\",\"Budai\",\"布袋\",\"布袋\",\"布袋\",\"布袋\",\"布袋\"\n\"C0M760\",\"民雄\",\"Minxiong\",\"民雄\",\"民雄\",\"民雄\",\"民雄\",\"民雄\"\n\"C0M770\",\"嘉義梅山\",\"Meishan Chiayi County\",\"嘉義梅山\",\"嘉義梅山\",\"嘉義梅山\",\"嘉義梅山\",\"嘉义梅山\"\n\"C0M780\",\"鹿草\",\"Lucao\",\"鹿草\",\"鹿草\",\"鹿草\",\"鹿草\",\"鹿草\"\n\"C0M790\",\"新港\",\"Xingang\",\"新港\",\"新港\",\"新港\",\"新港\",\"新港\"\n\"C0M800\",\"茶山\",\"Chashan\",\"茶山\",\"茶山\",\"茶山\",\"茶山\",\"茶山\"\n\"C0M810\",\"里佳\",\"Lijia\",\"里佳\",\"里佳\",\"里佳\",\"里佳\",\"里佳\"\n\"C0M820\",\"達邦\",\"Dabang\",\"達邦\",\"達邦\",\"達邦\",\"達邦\",\"达邦\"\n\"C0M850\",\"表湖\",\"Biaohu\",\"表湖\",\"表湖\",\"表湖\",\"表湖\",\"表湖\"\n\"C0M860\",\"新美\",\"Shinmei\",\"新美\",\"新美\",\"新美\",\"新美\",\"新美\"\n\"C0M880\",\"好美里\",\"Haomeili\",\"好美里\",\"好美里\",\"好美里\",\"好美里\",\"好美里\"\n\"C0N010\",\"鯤鯓國小\",\"Kunshen Elementary School\",\"鯤鯓國小\",\"鯤鯓國小\",\"鯤鯓國小\",\"鯤鯓國小\",\"鲲鯓国小\"\n\"C0N020\",\"城西\",\"Chengsi\",\"城西\",\"城西\",\"城西\",\"城西\",\"城西\"\n\"C0N030\",\"四草\",\"Sihtsao\",\"四草\",\"四草\",\"四草\",\"四草\",\"四草\"\n\"C0N040\",\"蘆竹溝\",\"Lujhugou\",\"蘆竹溝\",\"蘆竹溝\",\"蘆竹溝\",\"蘆竹溝\",\"芦竹沟\"\n\"C0N050\",\"蚵寮\",\"Eliao\",\"蚵寮\",\"蚵寮\",\"蚵寮\",\"蚵寮\",\"蚵寮\"\n\"C0O830\",\"北寮\",\"Beiliao\",\"北寮\",\"北寮\",\"北寮\",\"北寮\",\"北寮\"\n\"C0O840\",\"王爺宮\",\"Wangyegong\",\"王爺宮\",\"王爺宮\",\"王爺宮\",\"王爺宮\",\"王爷宫\"\n\"C0O860\",\"大內\",\"Danei\",\"大内\",\"大內\",\"大內\",\"大內\",\"大内\"\n\"C0O900\",\"善化\",\"Shanhua\",\"善化\",\"善化\",\"善化\",\"善化\",\"善化\"\n\"C0O960\",\"崎頂\",\"Qiding\",\"崎頂\",\"崎頂\",\"崎頂\",\"崎頂\",\"崎顶\"\n\"C0O970\",\"虎頭埤\",\"Hutoupi\",\"虎頭埤\",\"虎頭埤\",\"虎頭埤\",\"虎頭埤\",\"虎头埤\"\n\"C0O980\",\"新市\",\"Xinshi\",\"新市\",\"新市\",\"新市\",\"新市\",\"新市\"\n\"C0O990\",\"媽廟\",\"Mamiao\",\"媽廟\",\"媽廟\",\"媽廟\",\"媽廟\",\"妈庙\"\n\"C0R100\",\"尾寮山\",\"Weiliaoshan\",\"尾寮山\",\"尾寮山\",\"尾寮山\",\"尾寮山\",\"尾寮山\"\n\"C0R130\",\"阿禮\",\"Ali\",\"阿禮\",\"阿禮\",\"阿禮\",\"阿禮\",\"阿礼\"\n\"C0R140\",\"瑪家\",\"Majia\",\"瑪家\",\"瑪家\",\"瑪家\",\"瑪家\",\"玛家\"\n\"C0R150\",\"三地門\",\"Sandimen\",\"三地門\",\"三地門\",\"三地門\",\"三地門\",\"三地门\"\n\"C0R160\",\"鹽埔\",\"Yanpuxinwei\",\"塩埔\",\"鹽埔\",\"鹽埔\",\"鹽埔\",\"盐埔\"\n\"C0R190\",\"赤山\",\"Chishan\",\"赤山\",\"赤山\",\"赤山\",\"赤山\",\"赤山\"\n\"C0R220\",\"潮州\",\"Chaojhou\",\"潮州\",\"潮州\",\"潮州\",\"潮州\",\"潮州\"\n\"C0R240\",\"來義\",\"Laiyi\",\"来義\",\"來義\",\"來義\",\"來義\",\"来义\"\n\"C0R260\",\"春日\",\"Chunri\",\"春日\",\"春日\",\"春日\",\"春日\",\"春日\"\n\"C0R270\",\"琉球嶼\",\"Liouciouyu\",\"琉球嶼\",\"琉球嶼\",\"琉球嶼\",\"琉球嶼\",\"琉球屿\"\n\"C0R280\",\"檳榔\",\"Binlang\",\"檳榔\",\"檳榔\",\"檳榔\",\"檳榔\",\"槟榔\"\n\"C0R320\",\"車城\",\"Checheng\",\"車城\",\"車城\",\"車城\",\"車城\",\"车城\"\n\"C0R341\",\"牡丹\",\"Mudan\",\"牡丹\",\"牡丹\",\"牡丹\",\"牡丹\",\"牡丹\"\n\"C0R350\",\"貓鼻頭\",\"Maobitou\",\"貓鼻頭\",\"貓鼻頭\",\"貓鼻頭\",\"貓鼻頭\",\"猫鼻头\"\n\"C0R440\",\"大漢山\",\"Dahanshan\",\"大漢山\",\"大漢山\",\"大漢山\",\"大漢山\",\"大汉山\"\n\"C0R470\",\"高樹\",\"Gaoshu\",\"高樹\",\"高樹\",\"高樹\",\"高樹\",\"高树\"\n\"C0R480\",\"長治\",\"Changzhi\",\"長治\",\"長治\",\"長治\",\"長治\",\"长治\"\n\"C0R490\",\"九如\",\"Jiuru\",\"九如\",\"九如\",\"九如\",\"九如\",\"九如\"\n\"C0R520\",\"崁頂\",\"Kanding\",\"崁頂\",\"崁頂\",\"崁頂\",\"崁頂\",\"崁顶\"\n\"C0R540\",\"佳冬\",\"Jiadong\",\"佳冬\",\"가동\",\"佳冬\",\"佳冬\",\"佳冬\"\n\"C0R550\",\"新埤\",\"Xinpi\",\"新埤\",\"新埤\",\"新埤\",\"新埤\",\"新埤\"\n\"C0R560\",\"新園\",\"Xinyuan\",\"新園\",\"新園\",\"新園\",\"新園\",\"新园\"\n\"C0R570\",\"麟洛\",\"Linluo\",\"麟洛\",\"麟洛\",\"麟洛\",\"麟洛\",\"麟洛\"\n\"C0R580\",\"南州\",\"Nanzhou\",\"南州\",\"南州\",\"南州\",\"南州\",\"南州\"\n\"C0R590\",\"里港\",\"Ligang\",\"里港\",\"里港\",\"里港\",\"里港\",\"里港\"\n\"C0R600\",\"舊泰武\",\"Jiutaiwu\",\"舊泰武\",\"舊泰武\",\"舊泰武\",\"舊泰武\",\"旧泰武\"\n\"C0R620\",\"墾雷\",\"Kenlei\",\"墾雷\",\"墾雷\",\"墾雷\",\"墾雷\",\"垦雷\"\n\"C0R640\",\"東港\",\"Donggang\",\"東港\",\"東港\",\"東港\",\"東港\",\"东港\"\n\"C0R650\",\"竹田\",\"Zhutian\",\"竹田\",\"竹田\",\"竹田\",\"竹田\",\"竹田\"\n\"C0R660\",\"枋寮\",\"Fangliao\",\"枋寮\",\"枋寮\",\"枋寮\",\"枋寮\",\"枋寮\"\n\"C0R670\",\"楓港\",\"Fenggang\",\"楓港\",\"楓港\",\"楓港\",\"楓港\",\"枫港\"\n\"C0R680\",\"佳樂水\",\"Jialeshui\",\"佳樂水\",\"佳樂水\",\"佳樂水\",\"佳樂水\",\"佳乐水\"\n\"C0R690\",\"墾丁\",\"Kenting\",\"墾丁\",\"墾丁\",\"墾丁\",\"墾丁\",\"垦丁\"\n\"C0R700\",\"枋山\",\"Fangshan\",\"枋山\",\"枋山\",\"枋山\",\"枋山\",\"枋山\"\n\"C0R710\",\"龍磐\",\"Longpan\",\"龍磐\",\"龍磐\",\"龍磐\",\"龍磐\",\"龙磐\"\n\"C0R720\",\"旭海\",\"Xuhai\",\"旭海\",\"旭海\",\"旭海\",\"旭海\",\"旭海\"\n\"C0R730\",\"大坪頂\",\"Dapingding\",\"大坪頂\",\"大坪頂\",\"大坪頂\",\"大坪頂\",\"大坪顶\"\n\"C0R741\",\"獅子\",\"Shizi\",\"獅子\",\"獅子\",\"獅子\",\"獅子\",\"狮子\"\n\"C0R750\",\"四林格山\",\"Silingeshan\",\"四林格山\",\"四林格山\",\"四林格山\",\"四林格山\",\"四林格山\"\n\"C0R760\",\"南仁湖\",\"Nanrenhu\",\"南仁湖\",\"南仁湖\",\"南仁湖\",\"南仁湖\",\"南仁湖\"\n\"C0R770\",\"保力\",\"Baoli\",\"保力\",\"保力\",\"保力\",\"保力\",\"保力\"\n\"C0R780\",\"滿州\",\"Manzhou\",\"満州\",\"滿州\",\"滿州\",\"滿州\",\"满州\"\n\"C0R790\",\"九棚\",\"Jiupeng\",\"九棚\",\"九棚\",\"九棚\",\"九棚\",\"九棚\"\n\"C0R800\",\"丹路\",\"Danlu\",\"丹路\",\"丹路\",\"丹路\",\"丹路\",\"丹路\"\n\"C0R810\",\"內獅\",\"Neishi\",\"內獅\",\"內獅\",\"內獅\",\"內獅\",\"内狮\"\n\"C0R820\",\"白鷺\",\"Bailu\",\"白鷺\",\"白鷺\",\"白鷺\",\"白鷺\",\"白鹭\"\n\"C0R830\",\"高士\",\"Gaoshi\",\"高士\",\"高士\",\"高士\",\"高士\",\"高士\"\n\"C0R840\",\"牡丹池山\",\"Mudanchisahn\",\"牡丹池山\",\"牡丹池山\",\"牡丹池山\",\"牡丹池山\",\"牡丹池山\"\n\"C0R850\",\"林邊\",\"Linbian\",\"林辺\",\"林邊\",\"林邊\",\"林邊\",\"林边\"\n\"C0R860\",\"鼻頭\",\"Bitou\",\"鼻頭\",\"鼻頭\",\"鼻頭\",\"鼻頭\",\"鼻头\"\n\"C0R870\",\"興海\",\"Singhai\",\"興海\",\"興海\",\"興海\",\"興海\",\"兴海\"\n\"C0R880\",\"後壁湖\",\"Houbihu\",\"後壁湖\",\"後壁湖\",\"後壁湖\",\"後壁湖\",\"后壁湖\"\n\"C0R890\",\"山海\",\"Shanhai\",\"山海\",\"山海\",\"山海\",\"山海\",\"山海\"\n\"C0R900\",\"竹坑\",\"Jhukeng\",\"竹坑\",\"竹坑\",\"竹坑\",\"竹坑\",\"竹坑\"\n\"C0R910\",\"下寮\",\"Sialiao\",\"下寮\",\"下寮\",\"下寮\",\"下寮\",\"下寮\"\n\"C0R920\",\"塭仔\",\"Wunzai\",\"塭仔\",\"塭仔\",\"塭仔\",\"塭仔\",\"塭仔\"\n\"C0R930\",\"萬丹\",\"Wandan\",\"万丹\",\"萬丹\",\"萬丹\",\"萬丹\",\"万丹\"\n\"C0R940\",\"加祿堂\",\"Jialutang\",\"加祿堂\",\"加祿堂\",\"加祿堂\",\"加祿堂\",\"加禄堂\"\n\"C0R950\",\"萬隆國小\",\"Wanlongguoxiao\",\"萬隆國小\",\"萬隆國小\",\"萬隆國小\",\"萬隆國小\",\"万隆国小\"\n\"C0R960\",\"內埔\",\"Neipu\",\"内埔\",\"內埔\",\"內埔\",\"內埔\",\"内埔\"\n\"C0S660\",\"下馬\",\"Xiama\",\"下馬\",\"下馬\",\"下馬\",\"下馬\",\"下马\"\n\"C0S690\",\"太麻里\",\"Taimali\",\"太麻里\",\"太麻里\",\"太麻里\",\"太麻里\",\"太麻里\"\n\"C0S700\",\"知本\",\"Jhihben\",\"知本\",\"知本\",\"知本\",\"知本\",\"知本\"\n\"C0S710\",\"鹿野\",\"Luye\",\"鹿野\",\"鹿野\",\"鹿野\",\"鹿野\",\"鹿野\"\n\"C0S730\",\"綠島\",\"Ludao\",\"緑島\",\"뤽다오\",\"綠島\",\"綠島\",\"绿岛\"\n\"C0S740\",\"池上\",\"Chihshang\",\"池上\",\"池上\",\"池上\",\"池上\",\"池上\"\n\"C0S750\",\"向陽\",\"Siangyang\",\"向陽\",\"向陽\",\"向陽\",\"向陽\",\"向阳\"\n\"C0S760\",\"紅石\",\"Hongshih\",\"紅石\",\"紅石\",\"紅石\",\"紅石\",\"红石\"\n\"C0S770\",\"大溪山\",\"Dasishan\",\"大溪山\",\"大溪山\",\"大溪山\",\"大溪山\",\"大溪山\"\n\"C0S790\",\"金崙\",\"Jinlun\",\"金崙\",\"金崙\",\"金崙\",\"金崙\",\"金仑\"\n\"C0S810\",\"東河\",\"Donghe\",\"東河\",\"東河\",\"東河\",\"東河\",\"东河\"\n\"C0S830\",\"長濱\",\"Changbin\",\"長浜\",\"長濱\",\"長濱\",\"長濱\",\"长滨\"\n\"C0S840\",\"南田\",\"Nantian\",\"南田\",\"南田\",\"南田\",\"南田\",\"南田\"\n\"C0S890\",\"關山\",\"Guanshan\",\"関山\",\"關山\",\"關山\",\"關山\",\"关山\"\n\"C0S900\",\"蘭嶼高中\",\"Lanyu High School\",\"蘭嶼高中\",\"蘭嶼高中\",\"蘭嶼高中\",\"蘭嶼高中\",\"兰屿高中\"\n\"C0S910\",\"蘭嶼燈塔\",\"Lanyu Lighthouse\",\"蘭嶼燈塔\",\"蘭嶼燈塔\",\"蘭嶼燈塔\",\"蘭嶼燈塔\",\"兰屿灯塔\"\n\"C0S920\",\"金峰嘉蘭\",\"Jialan Jinfeng\",\"金峰嘉蘭\",\"金峰嘉蘭\",\"金峰嘉蘭\",\"金峰嘉蘭\",\"金峰嘉兰\"\n\"C0S930\",\"延平\",\"Yanping\",\"延平\",\"延平\",\"延平\",\"延平\",\"延平\"\n\"C0S940\",\"石寧山\",\"Shiningshan\",\"石寧山\",\"石寧山\",\"石寧山\",\"石寧山\",\"石宁山\"\n\"C0S950\",\"七塊厝\",\"Qikuaicuo\",\"七塊厝\",\"七塊厝\",\"七塊厝\",\"七塊厝\",\"七块厝\"\n\"C0S960\",\"香蘭\",\"Xianglan\",\"香蘭\",\"香蘭\",\"香蘭\",\"香蘭\",\"香兰\"\n\"C0S970\",\"加津林\",\"Jiajinlin\",\"加津林\",\"加津林\",\"加津林\",\"加津林\",\"加津林\"\n\"C0S980\",\"勝林山\",\"Shenglinshan\",\"勝林山\",\"勝林山\",\"勝林山\",\"勝林山\",\"胜林山\"\n\"C0S990\",\"山豬窟\",\"Shanzhuku\",\"山豬窟\",\"山豬窟\",\"山豬窟\",\"山豬窟\",\"山猪窟\"\n\"C0SA00\",\"歷坵\",\"Liqiu\",\"歷坵\",\"歷坵\",\"歷坵\",\"歷坵\",\"历坵\"\n\"C0SA10\",\"檳榔四格山\",\"Binlangsigeshan\",\"檳榔四格山\",\"檳榔四格山\",\"檳榔四格山\",\"檳榔四格山\",\"槟榔四格山\"\n\"C0SA20\",\"金崙山\",\"Jinlunshan\",\"金崙山\",\"金崙山\",\"金崙山\",\"金崙山\",\"金仑山\"\n\"C0SA30\",\"都歷\",\"Duli\",\"都歷\",\"都歷\",\"都歷\",\"都歷\",\"都历\"\n\"C0SA40\",\"瑞和\",\"Ruihe\",\"瑞和\",\"瑞和\",\"瑞和\",\"瑞和\",\"瑞和\"\n\"C0SA60\",\"知本（水試所）\",\"Zhiben (FRI)\",\"知本（水試所）\",\"知本（水試所）\",\"知本（水試所）\",\"知本（水試所）\",\"知本（水试所）\"\n\"C0SA80\",\"土坂\",\"Tuban\",\"土坂\",\"土坂\",\"土坂\",\"土坂\",\"土坂\"\n\"C0SA90\",\"達仁林場\",\"Darenlinchang\",\"達仁林場\",\"達仁林場\",\"達仁林場\",\"達仁林場\",\"达仁林场\"\n\"C0SB10\",\"美和\",\"Meihe\",\"美和\",\"美和\",\"美和\",\"美和\",\"美和\"\n\"C0SB20\",\"富岡\",\"Fugang\",\"富岡\",\"富岡\",\"富岡\",\"富岡\",\"富冈\"\n\"C0SB30\",\"新蘭\",\"Dulan Fire Brigade\",\"新蘭\",\"新蘭\",\"新蘭\",\"新蘭\",\"新兰\"\n\"C0SB40\",\"興隆\",\" Xinglong\",\"興隆\",\"興隆\",\"興隆\",\"興隆\",\"兴隆\"\n\"C0SB50\",\"叭嗡嗡\",\"Baweng\",\"叭嗡嗡\",\"叭嗡嗡\",\"叭嗡嗡\",\"叭嗡嗡\",\"叭嗡嗡\"\n\"C0SB60\",\"白守蓮\",\"Baishoulian\",\"白守蓮\",\"白守蓮\",\"白守蓮\",\"白守蓮\",\"白守莲\"\n\"C0SB70\",\"小港漁港\",\"Xiaogang Fishing Harbor\",\"小港漁港\",\"小港漁港\",\"小港漁港\",\"小港漁港\",\"小港渔港\"\n\"C0SB80\",\"長濱漁港\",\"Changbin Fishing Harbor\",\"長濱漁港\",\"長濱漁港\",\"長濱漁港\",\"長濱漁港\",\"长滨渔港\"\n\"C0T790\",\"大禹嶺\",\"Dayuling\",\"大禹嶺\",\"大禹嶺\",\"大禹嶺\",\"大禹嶺\",\"大禹岭\"\n\"C0T820\",\"天祥\",\"Tianxiang\",\"天祥\",\"天祥\",\"天祥\",\"天祥\",\"天祥\"\n\"C0T870\",\"鯉魚潭\",\"Liyutan\",\"鯉魚潭\",\"鯉魚潭\",\"鯉魚潭\",\"鯉魚潭\",\"鲤鱼潭\"\n\"C0T900\",\"西林\",\"Xilin\",\"西林\",\"西林\",\"西林\",\"西林\",\"西林\"\n\"C0T960\",\"光復\",\"Guangfu\",\"光復\",\"光復\",\"光復\",\"光復\",\"光复\"\n\"C0T9A0\",\"月眉山\",\"Yuemeishan\",\"月眉山\",\"月眉山\",\"月眉山\",\"月眉山\",\"月眉山\"\n\"C0T9B0\",\"水源\",\"Shuiyuan\",\"水源\",\"水源\",\"水源\",\"水源\",\"水源\"\n\"C0T9D0\",\"和中\",\"Hezhong\",\"和中\",\"和中\",\"和中\",\"和中\",\"和中\"\n\"C0T9E0\",\"大坑\",\"Dakeng\",\"大坑\",\"大坑\",\"大坑\",\"大坑\",\"大坑\"\n\"C0T9F0\",\"水璉\",\"Shuilian\",\"水璉\",\"水璉\",\"水璉\",\"水璉\",\"水琏\"\n\"C0T9G0\",\"鳳林山\",\"Fenglinshan\",\"鳳林山\",\"鳳林山\",\"鳳林山\",\"鳳林山\",\"凤林山\"\n\"C0T9H0\",\"加路蘭山\",\"Jialulanshan\",\"加路蘭山\",\"加路蘭山\",\"加路蘭山\",\"加路蘭山\",\"加路兰山\"\n\"C0T9I0\",\"豐濱\",\"Fengbin\",\"豊浜\",\"豐濱\",\"豐濱\",\"豐濱\",\"丰滨\"\n\"C0T9M0\",\"靜浦\",\"Jingpu\",\"靜浦\",\"靜浦\",\"靜浦\",\"靜浦\",\"静埔\"\n\"C0T9N0\",\"富里\",\"Fuli\",\"富里\",\"富里\",\"富里\",\"富里\",\"富里\"\n\"C0TA10\",\"花蓮漁港\",\"Hualien Fishing Harbor\",\"花蓮漁港\",\"花蓮漁港\",\"花蓮漁港\",\"花蓮漁港\",\"花莲渔港\"\n\"C0TA20\",\"加灣\",\"Jiawan\",\"加灣\",\"加灣\",\"加灣\",\"加灣\",\"加湾\"\n\"C0TA30\",\"鹽寮\",\"Yanliao\",\"鹽寮\",\"鹽寮\",\"鹽寮\",\"鹽寮\",\"盐寮\"\n\"C0TA40\",\"秀林\",\"Xiulin\",\"秀林\",\"秀林\",\"秀林\",\"秀林\",\"秀林\"\n\"C0TA50\",\"和仁\",\"Heren\",\"和仁\",\"和仁\",\"和仁\",\"和仁\",\"和仁\"\n\"C0TA80\",\"立霧山\",\"Liwushan\",\"立霧山\",\"立霧山\",\"立霧山\",\"立霧山\",\"立雾山\"\n\"C0U520\",\"雙連埤\",\"Shuanglianpi\",\"雙連埤\",\"雙連埤\",\"雙連埤\",\"雙連埤\",\"双连埤\"\n\"C0U600\",\"礁溪\",\"Chiaoshi\",\"礁渓\",\"礁溪\",\"礁溪\",\"礁溪\",\"礁溪\"\n\"C0U650\",\"玉蘭\",\"Yulan\",\"玉蘭\",\"玉蘭\",\"玉蘭\",\"玉蘭\",\"玉兰\"\n\"C0U710\",\"太平山\",\"Taipingshan\",\"太平山\",\"太平山\",\"太平山\",\"太平山\",\"太平山\"\n\"C0U720\",\"南山\",\"Nanshan\",\"南山\",\"南山\",\"南山\",\"南山\",\"南山\"\n\"C0U750\",\"龜山島\",\"Gueishandao\",\"龜山島\",\"龜山島\",\"龜山島\",\"龜山島\",\"龟山岛\"\n\"C0U760\",\"東澳\",\"Dong-Ao\",\"東澳\",\"東澳\",\"東澳\",\"東澳\",\"东澳\"\n\"C0U770\",\"南澳\",\"Nanao\",\"南澳\",\"南澳\",\"南澳\",\"南澳\",\"南澳\"\n\"C0U780\",\"五結\",\"Wujie\",\"五結\",\"五結\",\"五結\",\"五結\",\"五结\"\n\"C0U860\",\"頭城\",\"Toucheng\",\"頭城\",\"頭城\",\"頭城\",\"頭城\",\"头城\"\n\"C0U870\",\"大礁溪\",\"Dajiaoxi\",\"大礁溪\",\"大礁溪\",\"大礁溪\",\"大礁溪\",\"大礁溪\"\n\"C0U880\",\"北關\",\"Beiguan\",\"北關\",\"北關\",\"北關\",\"北關\",\"北关\"\n\"C0U890\",\"三星\",\"Sanxing\",\"三星\",\"三星\",\"三星\",\"三星\",\"三星\"\n\"C0U900\",\"內城\",\"Neicheng\",\"內城\",\"內城\",\"內城\",\"內城\",\"内城\"\n\"C0U910\",\"冬山\",\"Dongshan\",\"冬山\",\"冬山\",\"冬山\",\"冬山\",\"冬山\"\n\"C0U940\",\"羅東\",\"Luodong\",\"羅東\",\"羅東\",\"羅東\",\"羅東\",\"罗东\"\n\"C0U950\",\"鶯子嶺\",\"Yingziling\",\"鶯子嶺\",\"鶯子嶺\",\"鶯子嶺\",\"鶯子嶺\",\"莺子岭\"\n\"C0U960\",\"翠峰湖\",\"Cuifenghu\",\"翠峰湖\",\"翠峰湖\",\"翠峰湖\",\"翠峰湖\",\"翠峰湖\"\n\"C0U970\",\"大福\",\"Dafu\",\"大福\",\"大福\",\"大福\",\"大福\",\"大福\"\n\"C0U980\",\"坪林石牌\",\"Shipai Pinglin\",\"坪林石牌\",\"坪林石牌\",\"坪林石牌\",\"坪林石牌\",\"坪林石牌\"\n\"C0U990\",\"員山\",\"Yuanshan\",\"員山\",\"員山\",\"員山\",\"員山\",\"员山\"\n\"C0UA00\",\"土場\",\"Tuchang\",\"土場\",\"土場\",\"土場\",\"土場\",\"土场\"\n\"C0UA10\",\"鴛鴦湖\",\"Yuanyanghu\",\"鴛鴦湖\",\"鴛鴦湖\",\"鴛鴦湖\",\"鴛鴦湖\",\"鸳鸯湖\"\n\"C0UA20\",\"多加屯\",\"Duojiatun\",\"多加屯\",\"多加屯\",\"多加屯\",\"多加屯\",\"多加屯\"\n\"C0UA30\",\"白嶺\",\"Bailing\",\"白嶺\",\"白嶺\",\"白嶺\",\"白嶺\",\"白岭\"\n\"C0UA40\",\"西德山\",\"Xideshan\",\"西德山\",\"西德山\",\"西德山\",\"西德山\",\"西德山\"\n\"C0UA50\",\"西帽山\",\"Ximaoshan\",\"西帽山\",\"西帽山\",\"西帽山\",\"西帽山\",\"西帽山\"\n\"C0UA60\",\"樟樹山\",\"Zhangshushan\",\"樟樹山\",\"樟樹山\",\"樟樹山\",\"樟樹山\",\"樟树山\"\n\"C0UA70\",\"桃源谷\",\"Taoyuangu\",\"桃源谷\",\"桃源谷\",\"桃源谷\",\"桃源谷\",\"桃源谷\"\n\"C0UA80\",\"大溪漁港\",\"Dasi Fishing Harbor\",\"大溪漁港\",\"大溪漁港\",\"大溪漁港\",\"大溪漁港\",\"大溪渔港\"\n\"C0UA90\",\"石城\",\"Shihcheng\",\"石城\",\"石城\",\"石城\",\"石城\",\"石城\"\n\"C0UB00\",\"淡江大學蘭陽校園\",\"Tamkang Lanyang Campus\",\"淡江大学蘭陽キャンパス\",\"淡江大學蘭陽校園\",\"淡江大學蘭陽校園\",\"淡江大學蘭陽校園\",\"淡江大学兰阳校园\"\n\"C0UB10\",\"蘇澳\",\"Suao\",\"蘇澳\",\"蘇澳\",\"蘇澳\",\"蘇澳\",\"苏澳\"\n\"C0UB20\",\"壯圍\",\"Jhuangwei\",\"壮囲\",\"壯圍\",\"壯圍\",\"壯圍\",\"壮围\"\n\"C0UB60\",\"明池\",\"Mingchr\",\"明池\",\"明池\",\"明池\",\"明池\",\"明池\"\n\"C0UB70\",\"太平山中間站\",\"Jhongjian\",\"太平山中間站\",\"太平山中間站\",\"太平山中間站\",\"太平山中間站\",\"太平山中间站\"\n\"C0UB80\",\"翠峰林道6K\",\"Trifong 6k\",\"翠峰林道6K\",\"翠峰林道6K\",\"翠峰林道6K\",\"翠峰林道6K\",\"翠峰林道6K\"\n\"C0UB90\",\"太平山莊\",\"Taipingshan Villa\",\"太平山莊\",\"太平山莊\",\"太平山莊\",\"太平山莊\",\"太平山庄\"\n\"C0V210\",\"復興\",\"Fuxing\",\"復興\",\"復興\",\"復興\",\"復興\",\"复兴\"\n\"C0V350\",\"溪埔\",\"Xipu\",\"溪埔\",\"溪埔\",\"溪埔\",\"溪埔\",\"溪埔\"\n\"C0V360\",\"內門\",\"Neimen\",\"内門\",\"內門\",\"內門\",\"內門\",\"内门\"\n\"C0V370\",\"古亭坑\",\"Gutingkeng\",\"古亭坑\",\"古亭坑\",\"古亭坑\",\"古亭坑\",\"古亭坑\"\n\"C0V400\",\"阿公店\",\"Agongdian\",\"阿公店\",\"阿公店\",\"阿公店\",\"阿公店\",\"阿公店\"\n\"C0V440\",\"鳳山\",\"Fengshan\",\"鳳山\",\"鳳山\",\"鳳山\",\"鳳山\",\"凤山\"\n\"C0V450\",\"鳳森\",\"Fengsen\",\"鳳森\",\"鳳森\",\"鳳森\",\"鳳森\",\"凤森\"\n\"C0V490\",\"新興\",\"Sinsing\",\"新興\",\"新興\",\"新興\",\"新興\",\"新兴\"\n\"C0V530\",\"阿蓮\",\"Alian\",\"阿蓮\",\"阿蓮\",\"阿蓮\",\"阿蓮\",\"阿连\"\n\"C0V610\",\"梓官\",\"Ziguan\",\"梓官\",\"梓官\",\"梓官\",\"梓官\",\"梓官\"\n\"C0V620\",\"永安\",\"Yong'An\",\"永安\",\"永安\",\"永安\",\"永安\",\"永安\"\n\"C0V630\",\"茄萣\",\"Qieding\",\"茄萣\",\"茄萣\",\"茄萣\",\"茄萣\",\"茄萣\"\n\"C0V640\",\"湖內\",\"Hunei\",\"湖内\",\"湖內\",\"湖內\",\"湖內\",\"湖内\"\n\"C0V650\",\"彌陀\",\"Mituo\",\"弥陀\",\"彌陀\",\"彌陀\",\"彌陀\",\"弥陀\"\n\"C0V660\",\"岡山\",\"Gangshan\",\"岡山\",\"岡山\",\"岡山\",\"岡山\",\"冈山\"\n\"C0V680\",\"仁武\",\"Renwu\",\"仁武\",\"仁武\",\"仁武\",\"仁武\",\"仁武\"\n\"C0V690\",\"鼓山\",\"Gushan\",\"鼓山\",\"鼓山\",\"鼓山\",\"鼓山\",\"鼓山\"\n\"C0V700\",\"三民\",\"Sanmin\",\"三民\",\"三民\",\"三民\",\"三民\",\"三民\"\n\"C0V710\",\"苓雅\",\"Lingya\",\"苓雅\",\"苓雅\",\"苓雅\",\"苓雅\",\"苓雅\"\n\"C0V720\",\"林園\",\"Linyuan\",\"林園\",\"林園\",\"林園\",\"林園\",\"林园\"\n\"C0V730\",\"大寮\",\"Daliao\",\"大寮\",\"大寮\",\"大寮\",\"大寮\",\"大寮\"\n\"C0V740\",\"旗山\",\"Qishan\",\"旗山\",\"旗山\",\"旗山\",\"旗山\",\"旗山\"\n\"C0V750\",\"路竹\",\"Luzhu\",\"路竹\",\"路竹\",\"路竹\",\"路竹\",\"路竹\"\n\"C0V760\",\"橋頭\",\"Qiaotou\",\"橋頭\",\"橋頭\",\"橋頭\",\"橋頭\",\"桥头\"\n\"C0V770\",\"大社\",\"Dashe\",\"大社\",\"大社\",\"大社\",\"大社\",\"大社\"\n\"C0V790\",\"萬山\",\"Wanshan\",\"萬山\",\"萬山\",\"萬山\",\"萬山\",\"万山\"\n\"C0V800\",\"六龜\",\"Liugui\",\"六亀\",\"六龜\",\"六龜\",\"六龜\",\"六龟\"\n\"C0V810\",\"左營\",\"Zuoying\",\"左営\",\"左營\",\"左營\",\"左營\",\"左营\"\n\"C0V820\",\"小林\",\"Xiaolin\",\"小林\",\"小林\",\"小林\",\"小林\",\"小林\"\n\"C0V840\",\"鳳鼻頭\",\"Fongbitou\",\"鳳鼻頭\",\"鳳鼻頭\",\"鳳鼻頭\",\"鳳鼻頭\",\"凤鼻头\"\n\"C0V850\",\"蚵仔寮\",\"Kezailiao\",\"蚵仔寮\",\"蚵仔寮\",\"蚵仔寮\",\"蚵仔寮\",\"蚵仔寮\"\n\"C0V860\",\"南寮\",\"Nanliao\",\"南寮\",\"南寮\",\"南寮\",\"南寮\",\"南寮\"\n\"C0V870\",\"文安\",\"Wunan\",\"文安\",\"文安\",\"文安\",\"文安\",\"文安\"\n\"C0V880\",\"興達\",\"Singda\",\"興達\",\"興達\",\"興達\",\"興達\",\"兴达\"\n\"C0V890\",\"前鎮\",\"Chian Jhen\",\"前鎮\",\"前鎮\",\"前鎮\",\"前鎮\",\"前镇\"\n\"C0V900\",\"汕尾\",\"Shanwei\",\"汕尾\",\"汕尾\",\"汕尾\",\"汕尾\",\"汕尾\"\n\"C0V910\",\"大樹\",\"Dashu\",\"大樹\",\"大樹\",\"大樹\",\"大樹\",\"大树\"\n\"C0W110\",\"東莒\",\"Dongju\",\"東莒\",\"東莒\",\"東莒\",\"東莒\",\"东莒\"\n\"C0W120\",\"西嶼\",\"Xiyu\",\"西嶼\",\"西嶼\",\"西嶼\",\"西嶼\",\"西屿\"\n\"C0W130\",\"花嶼\",\"Huayu\",\"花嶼\",\"花嶼\",\"花嶼\",\"花嶼\",\"花屿\"\n\"C0W140\",\"金沙\",\"Jinsha \",\"金沙\",\"金沙\",\"金沙\",\"金沙\",\"金沙\"\n\"C0W150\",\"金寧\",\"Jinning\",\"金寧\",\"金寧\",\"金寧\",\"金寧\",\"金宁\"\n\"C0W160\",\"烏坵\",\"Wuqiu\",\"烏坵\",\"烏坵\",\"烏坵\",\"烏坵\",\"乌坵\"\n\"C0W180\",\"七美\",\"Qimei\",\"七美\",\"七美\",\"七美\",\"七美\",\"七美\"\n\"C0W190\",\"望安\",\"Wangan\",\"望安\",\"望安\",\"望安\",\"望安\",\"望安\"\n\"C0W200\",\"湖西\",\"Husi\",\"湖西\",\"湖西\",\"湖西\",\"湖西\",\"湖西\"\n\"C0W220\",\"北竿\",\"Beigan\",\"北竿\",\"北竿\",\"北竿\",\"北竿\",\"北竿\"\n\"C0W240\",\"九宮\",\"Jiugong \",\"九宮\",\"九宮\",\"九宮\",\"九宮\",\"九宫\"\n\"C0X050\",\"東河\",\"Donghe\",\"東河\",\"東河\",\"東河\",\"東河\",\"东河\"\n\"C0X060\",\"下營\",\"Xiaying\",\"下営\",\"下營\",\"下營\",\"下營\",\"下营\"\n\"C0X080\",\"佳里\",\"Jiali\",\"佳里\",\"佳里\",\"佳里\",\"佳里\",\"佳里\"\n\"C0X100\",\"臺南市北區\",\"Beiqu Tainan City\",\"臺南市北區\",\"臺南市北區\",\"臺南市北區\",\"臺南市北區\",\"台南市北区\"\n\"C0X110\",\"臺南市南區\",\"Nanqu Tainan City\",\"臺南市南區\",\"臺南市南區\",\"臺南市南區\",\"臺南市南區\",\"台南市南区\"\n\"C0X120\",\"麻豆\",\"Madou\",\"麻豆\",\"麻豆\",\"麻豆\",\"麻豆\",\"麻豆\"\n\"C0X130\",\"官田\",\"Guantian\",\"官田\",\"官田\",\"官田\",\"官田\",\"官田\"\n\"C0X140\",\"西港\",\"Xigang\",\"西港\",\"西港\",\"西港\",\"西港\",\"西港\"\n\"C0X150\",\"安定\",\"Anding\",\"安定\",\"安定\",\"安定\",\"安定\",\"安定\"\n\"C0X160\",\"仁德\",\"Rende\",\"仁徳\",\"仁德\",\"仁德\",\"仁德\",\"仁德\"\n\"C0X170\",\"關廟\",\"Guanmiao\",\"関廟\",\"關廟\",\"關廟\",\"關廟\",\"关庙\"\n\"C0X180\",\"山上\",\"Shanshang\",\"山上\",\"山上\",\"山上\",\"山上\",\"山上\"\n\"C0X190\",\"安平\",\"Anping\",\"安平\",\"安平\",\"安平\",\"安平\",\"安平\"\n\"C0X200\",\"左鎮\",\"Zuozhen\",\"左鎮\",\"左鎮\",\"左鎮\",\"左鎮\",\"左镇\"\n\"C0X210\",\"白河\",\"Baihe\",\"白河\",\"白河\",\"白河\",\"白河\",\"白河\"\n\"C0X220\",\"學甲\",\"Xuejia\",\"学甲\",\"學甲\",\"學甲\",\"學甲\",\"学甲\"\n\"C0X230\",\"鹽水\",\"Yanshui\",\"塩水\",\"鹽水\",\"鹽水\",\"鹽水\",\"盐水\"\n\"C0X240\",\"關子嶺\",\"Guanziling\",\"關子嶺\",\"關子嶺\",\"關子嶺\",\"關子嶺\",\"关子岭\"\n\"C0X250\",\"新營\",\"Xinying\",\"新営\",\"新營\",\"新營\",\"新營\",\"新营\"\n\"C0X260\",\"後壁\",\"Houbi\",\"後壁\",\"後壁\",\"後壁\",\"後壁\",\"后壁\"\n\"C0X280\",\"將軍\",\"Jiangjun\",\"将軍\",\"將軍\",\"將軍\",\"將軍\",\"将军\"\n\"C0X290\",\"北門\",\"Beimen\",\"北門\",\"北門\",\"北門\",\"北門\",\"北门\"\n\"C0X300\",\"鹿寮\",\"Luliao\",\"鹿寮\",\"鹿寮\",\"鹿寮\",\"鹿寮\",\"鹿寮\"\n\"C0X320\",\"柳營\",\"Liuying\",\"柳営\",\"柳營\",\"柳營\",\"柳營\",\"柳营\"\n\"C0Z020\",\"明里\",\"Mingli\",\"明里\",\"明里\",\"明里\",\"明里\",\"明里\"\n\"C0Z050\",\"佳心\",\"Jiaxin\",\"佳心\",\"佳心\",\"佳心\",\"佳心\",\"佳心\"\n\"C0Z061\",\"玉里\",\"Yuli\",\"玉里\",\"玉里\",\"玉里\",\"玉里\",\"玉里\"\n\"C0Z070\",\"舞鶴\",\"Wuhe\",\"舞鶴\",\"舞鶴\",\"舞鶴\",\"舞鶴\",\"舞鹤\"\n\"C0Z080\",\"富源\",\"Fuyuan\",\"富源\",\"富源\",\"富源\",\"富源\",\"富源\"\n\"C0Z100\",\"東華\",\"Donghwa\",\"東華\",\"東華\",\"東華\",\"東華\",\"东华\"\n\"C0Z150\",\"吉安光華\",\"Guanghua Ji-An\",\"吉安光華\",\"吉安光華\",\"吉安光華\",\"吉安光華\",\"吉安光华\"\n\"C0Z160\",\"鳳林\",\"Fenglin\",\"鳳林\",\"鳳林\",\"鳳林\",\"鳳林\",\"凤林\"\n\"C0Z170\",\"卓溪\",\"Zhuoxi\",\"卓渓\",\"卓溪\",\"卓溪\",\"卓溪\",\"卓溪\"\n\"C0Z180\",\"新城\",\"Xincheng\",\"新城\",\"新城\",\"新城\",\"新城\",\"新城\"\n\"C0Z190\",\"富世\",\"Fushi\",\"富世\",\"富世\",\"富世\",\"富世\",\"富世\"\n\"C0Z200\",\"萬榮\",\"Wanrong\",\"万栄\",\"萬榮\",\"萬榮\",\"萬榮\",\"万荣\"\n\"C0Z210\",\"瑞穗\",\"Ruisui\",\"瑞穂\",\"瑞穗\",\"瑞穗\",\"瑞穗\",\"瑞穗\"\n\"C0Z220\",\"和平林道\",\"Hepinglindao\",\"和平林道\",\"和平林道\",\"和平林道\",\"和平林道\",\"和平林道\"\n\"C0Z230\",\"和平\",\"Heping\",\"和平\",\"허핑\",\"和平\",\"和平\",\"和平\"\n\"C0Z250\",\"瑞穗林道\",\"Ruisuilindao\",\"瑞穗林道\",\"瑞穗林道\",\"瑞穗林道\",\"瑞穗林道\",\"瑞穗林道\"\n\"C0Z270\",\"蕃薯寮\",\"Fanshuliao\",\"蕃薯寮\",\"蕃薯寮\",\"蕃薯寮\",\"蕃薯寮\",\"蕃薯寮\"\n\"C0Z280\",\"德武\",\"Dewu\",\"德武\",\"德武\",\"德武\",\"德武\",\"德武\"\n\"C0Z290\",\"赤柯山\",\"Chikeshan\",\"赤柯山\",\"赤柯山\",\"赤柯山\",\"赤柯山\",\"赤柯山\"\n\"C0Z300\",\"東里\",\"Dongli\",\"東里\",\"東里\",\"東里\",\"東里\",\"东里\"\n\"C0Z310\",\"清水斷崖\",\"Qingshui Cliff\",\"清水斷崖\",\"清水斷崖\",\"清水斷崖\",\"清水斷崖\",\"清水断崖\"\n\"C0Z320\",\"清水林道\",\"Qingshuilindao\",\"清水林道\",\"清水林道\",\"清水林道\",\"清水林道\",\"清水林道\"\n\"C0Z330\",\"安通山\",\"Antongshan\",\"安通山\",\"安通山\",\"安通山\",\"安通山\",\"安通山\"\n\"C1A630\",\"下盆\",\"Siapen\",\"下盆\",\"下盆\",\"下盆\",\"下盆\",\"下盆\"\n\"C1A750\",\"石碇服務區\",\"Shiding Service Area\",\"石碇服務區\",\"石碇服務區\",\"石碇服務區\",\"石碇服務區\",\"石碇服务区\"\n\"C1A760\",\"坪林交控\",\"Pinglin Traffic Control Center\",\"坪林交控\",\"坪林交控\",\"坪林交控\",\"坪林交控\",\"坪林交控\"\n\"C1A9N0\",\"四十份\",\"Sihshihfen\",\"四十份\",\"四十份\",\"四十份\",\"四十份\",\"四十份\"\n\"C1AC50\",\"關渡\",\"Guandu\",\"關渡\",\"關渡\",\"關渡\",\"關渡\",\"关渡\"\n\"C1AI50\",\"國三N016K\",\"Freeway No. 3 - Rain - N016k\",\"國三N016K\",\"國三N016K\",\"國三N016K\",\"國三N016K\",\"国三N016K\"\n\"C1AI60\",\"國一39K邊坡\",\"Freeway No. 1 - Rain – N039k\",\"國一39K邊坡\",\"國一39K邊坡\",\"國一39K邊坡\",\"國一39K邊坡\",\"国一39K边坡\"\n\"C1C510\",\"水尾\",\"Shueiwei\",\"水尾\",\"水尾\",\"水尾\",\"水尾\",\"水尾\"\n\"C1D380\",\"新埔\",\"Sinpu\",\"新埔\",\"新埔\",\"新埔\",\"新埔\",\"新埔\"\n\"C1D400\",\"鳥嘴山\",\"Niaozueishan\",\"鳥嘴山\",\"鳥嘴山\",\"鳥嘴山\",\"鳥嘴山\",\"鸟嘴山\"\n\"C1D410\",\"白蘭\",\"Bailan\",\"白蘭\",\"白蘭\",\"白蘭\",\"白蘭\",\"白兰\"\n\"C1D420\",\"太閣南\",\"Taigenan\",\"太閣南\",\"太閣南\",\"太閣南\",\"太閣南\",\"太阁南\"\n\"C1D630\",\"飛鳳山\",\"Fei Feng Mountain\",\"飛鳳山\",\"飛鳳山\",\"飛鳳山\",\"飛鳳山\",\"飞凤山\"\n\"C1D640\",\"外坪(五指山)\",\"Waiping(Wuzhihshan)\",\"外坪(五指山)\",\"外坪(五指山)\",\"外坪(五指山)\",\"外坪(五指山)\",\"外坪(五指山)\"\n\"C1E451\",\"象鼻\",\"Xiangbi\",\"象鼻\",\"象鼻\",\"象鼻\",\"象鼻\",\"象鼻\"\n\"C1E461\",\"松安\",\"Song-An\",\"松安\",\"松安\",\"松安\",\"松安\",\"松安\"\n\"C1E480\",\"鳳美\",\"Fongmei\",\"鳳美\",\"鳳美\",\"鳳美\",\"鳳美\",\"凤美\"\n\"C1E511\",\"新開\",\"Xinkai\",\"新開\",\"新開\",\"新開\",\"新開\",\"新开\"\n\"C1E601\",\"南勢\",\"Nanshi\",\"南勢\",\"南勢\",\"南勢\",\"南勢\",\"南势\"\n\"C1E670\",\"南礦\",\"Nankuang\",\"南礦\",\"南礦\",\"南礦\",\"南礦\",\"南矿\"\n\"C1E681\",\"南勢山\",\"Nanshishan\",\"南勢山\",\"南勢山\",\"南勢山\",\"南勢山\",\"南势山\"\n\"C1E691\",\"南湖\",\"Nanhu\",\"南湖\",\"南湖\",\"南湖\",\"南湖\",\"南胡\"\n\"C1E701\",\"八卦\",\"Bagua\",\"八卦\",\"八卦\",\"八卦\",\"八卦\",\"八卦\"\n\"C1E711\",\"馬拉邦山\",\"Malabangshan\",\"馬拉邦山\",\"馬拉邦山\",\"馬拉邦山\",\"馬拉邦山\",\"馬拉邦山\"\n\"C1E721\",\"泰安\",\"Tai-An\",\"泰安\",\"泰安\",\"泰安\",\"泰安\",\"泰安\"\n\"C1E770\",\"公館\",\"Gongguan\",\"公館\",\"公館\",\"公館\",\"公館\",\"公馆\"\n\"C1E890\",\"國三N149K\",\"Freeway No. 1 - Rain – N149k\",\"國三N149K\",\"國三N149K\",\"國三N149K\",\"國三N149K\",\"国三N149K\"\n\"C1E900\",\"國一N128K\",\"Freeway No. 1 - Rain – N128k\",\"國一N128K\",\"國一N128K\",\"國一N128K\",\"國一N128K\",\"国一N128K\"\n\"C1F871\",\"上谷關\",\"Shangguguan\",\"上谷關\",\"上谷關\",\"上谷關\",\"上谷關\",\"上谷关\"\n\"C1F891\",\"稍來\",\"Shaolai\",\"稍來\",\"稍來\",\"稍來\",\"稍來\",\"稍来\"\n\"C1F911\",\"新伯公\",\"Xinbogong\",\"新伯公\",\"新伯公\",\"新伯公\",\"新伯公\",\"新伯公\"\n\"C1F941\",\"雪嶺\",\"Xueling\",\"雪嶺\",\"雪嶺\",\"雪嶺\",\"雪嶺\",\"雪岭\"\n\"C1F9B1\",\"桐林\",\"Tonglin\",\"桐林\",\"桐林\",\"桐林\",\"桐林\",\"桐林\"\n\"C1F9C1\",\"白冷\",\"Baileng\",\"白冷\",\"白冷\",\"白冷\",\"白冷\",\"白冷\"\n\"C1F9D1\",\"白毛台\",\"Baimaotai\",\"白毛台\",\"白毛台\",\"白毛台\",\"白毛台\",\"白毛台\"\n\"C1F9E1\",\"龍安\",\"Long-An\",\"龍安\",\"龍安\",\"龍安\",\"龍安\",\"龙安\"\n\"C1F9F1\",\"伯公龍\",\"Bogonglong\",\"伯公龍\",\"伯公龍\",\"伯公龍\",\"伯公龍\",\"伯公龙\"\n\"C1F9G1\",\"慶福山\",\"Cingfushan\",\"慶福山\",\"慶福山\",\"慶福山\",\"慶福山\",\"庆福山\"\n\"C1F9J1\",\"清水林\",\"Qingshuilin\",\"清水林\",\"清水林\",\"清水林\",\"清水林\",\"清水林\"\n\"C1F9W0\",\"德基\",\"Deji\",\"德基\",\"德基\",\"德基\",\"德基\",\"德基\"\n\"C1G691\",\"下水埔\",\"Xiashuipu\",\"下水埔\",\"下水埔\",\"下水埔\",\"下水埔\",\"下水埔\"\n\"C1G9D0\",\"國一S218K\",\"Freeway No. 1 - Rain – S218k\",\"國一S218K\",\"國一S218K\",\"國一S218K\",\"國一S218K\",\"国一S218K\"\n\"C1H000\",\"翠峰\",\"Cuifeng\",\"翠峰\",\"翠峰\",\"翠峰\",\"翠峰\",\"翠峰\"\n\"C1H840\",\"國三N238K\",\"Freeway No. 3 - Rain –N238k\",\"國三N238K\",\"國三N238K\",\"國三N238K\",\"國三N238K\",\"国三N238K\"\n\"C1H900\",\"清流\",\"Qingliu\",\"清流\",\"清流\",\"清流\",\"清流\",\"清流\"\n\"C1H920\",\"長豐\",\"Changfeng\",\"長豐\",\"長豐\",\"長豐\",\"長豐\",\"长丰\"\n\"C1H941\",\"雙冬\",\"Shuangdong\",\"雙冬\",\"雙冬\",\"雙冬\",\"雙冬\",\"双冬\"\n\"C1H971\",\"六分寮\",\"Liufenliao\",\"六分寮\",\"六分寮\",\"六分寮\",\"六分寮\",\"六分寮\"\n\"C1H9B1\",\"阿眉\",\"Amei\",\"阿眉\",\"阿眉\",\"阿眉\",\"阿眉\",\"阿眉\"\n\"C1I020\",\"萬大\",\"Wanda\",\"萬大\",\"萬大\",\"萬大\",\"萬大\",\"万大\"\n\"C1I030\",\"武界\",\"Wujie\",\"武界\",\"武界\",\"武界\",\"武界\",\"武界\"\n\"C1I050\",\"丹大\",\"Danda\",\"丹大\",\"丹大\",\"丹大\",\"丹大\",\"丹大\"\n\"C1I070\",\"和社\",\"Heshe\",\"和社\",\"和社\",\"和社\",\"和社\",\"和社\"\n\"C1I101\",\"溪頭\",\"Xitou\",\"溪頭\",\"溪頭\",\"溪頭\",\"溪頭\",\"溪头\"\n\"C1I121\",\"大鞍\",\"Da-An\",\"大鞍\",\"大鞍\",\"大鞍\",\"大鞍\",\"大鞍\"\n\"C1I131\",\"桶頭\",\"Tongtou\",\"桶頭\",\"桶頭\",\"桶頭\",\"桶頭\",\"桶头\"\n\"C1I140\",\"卡奈托灣\",\"Kanaituowan\",\"卡奈托灣\",\"卡奈托灣\",\"卡奈托灣\",\"卡奈托灣\",\"卡奈托湾\"\n\"C1I150\",\"青雲\",\"Qingyun\",\"青雲\",\"青雲\",\"青雲\",\"青雲\",\"青云\"\n\"C1I201\",\"中心崙\",\"Zhongxinlun\",\"中心崙\",\"中心崙\",\"中心崙\",\"中心崙\",\"中心仑\"\n\"C1I211\",\"蘆竹湳\",\"Luzhunan\",\"蘆竹湳\",\"蘆竹湳\",\"蘆竹湳\",\"蘆竹湳\",\"芦竹湳\"\n\"C1I220\",\"樟湖\",\"Zhanghu\",\"樟湖\",\"樟湖\",\"樟湖\",\"樟湖\",\"樟湖\"\n\"C1I230\",\"九份二山\",\"Jiufen'Ershan\",\"九份二山\",\"九份二山\",\"九份二山\",\"九份二山\",\"九份二山\"\n\"C1I240\",\"外大坪\",\"Waidaping\",\"外大坪\",\"外大坪\",\"外大坪\",\"外大坪\",\"外大坪\"\n\"C1I250\",\"鯉潭\",\"Litan\",\"鯉潭\",\"鯉潭\",\"鯉潭\",\"鯉潭\",\"鲤潭\"\n\"C1I260\",\"北坑\",\"Beikeng\",\"北坑\",\"北坑\",\"北坑\",\"北坑\",\"北坑\"\n\"C1I280\",\"埔中\",\"Puzhong\",\"埔中\",\"埔中\",\"埔中\",\"埔中\",\"埔中\"\n\"C1I290\",\"豐丘\",\"Fengqiu\",\"豐丘\",\"豐丘\",\"豐丘\",\"豐丘\",\"丰丘\"\n\"C1I310\",\"西巒\",\"Xiluan\",\"西巒\",\"西巒\",\"西巒\",\"西巒\",\"西峦\"\n\"C1I320\",\"奧萬大\",\"Aowanda\",\"奧萬大\",\"奧萬大\",\"奧萬大\",\"奧萬大\",\"奥万大\"\n\"C1I330\",\"楓樹林\",\"Fengshulin\",\"楓樹林\",\"楓樹林\",\"楓樹林\",\"楓樹林\",\"枫树林\"\n\"C1I340\",\"新興橋\",\"Xinxingqiao\",\"新興橋\",\"新興橋\",\"新興橋\",\"新興橋\",\"新兴桥\"\n\"C1I400\",\"凌霄\",\"Lingxiao\",\"凌霄\",\"凌霄\",\"凌霄\",\"凌霄\",\"凌霄\"\n\"C1I430\",\"翠華\",\"Cuihua\",\"翠華\",\"翠華\",\"翠華\",\"翠華\",\"翠华\"\n\"C1I440\",\"新高口\",\"Xingaokou\",\"新高口\",\"新高口\",\"新高口\",\"新高口\",\"新高口\"\n\"C1I450\",\"望鄉山\",\"Wangxiangshan\",\"望鄉山\",\"望鄉山\",\"望鄉山\",\"望鄉山\",\"望乡山\"\n\"C1I470\",\"杉林溪\",\"Shanlinxi\",\"杉林溪\",\"杉林溪\",\"杉林溪\",\"杉林溪\",\"杉林溪\"\n\"C1I500\",\"大尖山\",\"Dajianshan\",\"大尖山\",\"大尖山\",\"大尖山\",\"大尖山\",\"大尖山\"\n\"C1I510\",\"線浸林道\",\"Xianjinlindao\",\"線浸林道\",\"線浸林道\",\"線浸林道\",\"線浸林道\",\"线浸林道\"\n\"C1I550\",\"國六W023K\",\"Freeway No. 6 - Rain – W023k\",\"國六W023K\",\"國六W023K\",\"國六W023K\",\"國六W023K\",\"国六W023K\"\n\"C1K540\",\"口湖\",\"Kouhu\",\"口湖\",\"口湖\",\"口湖\",\"口湖\",\"口湖\"\n\"C1M390\",\"龍美\",\"Longmei\",\"龍美\",\"龍美\",\"龍美\",\"龍美\",\"龙美\"\n\"C1M400\",\"菜瓜坪\",\"Caiguaping\",\"菜瓜坪\",\"菜瓜坪\",\"菜瓜坪\",\"菜瓜坪\",\"菜瓜坪\"\n\"C1M480\",\"獨立山\",\"Dulishan\",\"獨立山\",\"獨立山\",\"獨立山\",\"獨立山\",\"独立山\"\n\"C1M600\",\"頭凍\",\"Toudong\",\"頭凍\",\"頭凍\",\"頭凍\",\"頭凍\",\"头冻\"\n\"C1M610\",\"石磐龍\",\"Shipanlong\",\"石磐龍\",\"石磐龍\",\"石磐龍\",\"石磐龍\",\"石磐龙\"\n\"C1M640\",\"十字\",\"Shizi\",\"十字\",\"十字\",\"十字\",\"十字\",\"十字\"\n\"C1M870\",\"國三N285K\",\"Freeway No. 3 - Rain –N285k\",\"國三N285K\",\"國三N285K\",\"國三N285K\",\"國三N285K\",\"国三N285K\"\n\"C1N001\",\"沙崙\",\"Shalun\",\"沙崙\",\"沙崙\",\"沙崙\",\"沙崙\",\"沙仑\"\n\"C1O850\",\"環湖\",\"Huanhu\",\"環湖\",\"環湖\",\"環湖\",\"環湖\",\"环湖\"\n\"C1O870\",\"大棟山\",\"Dadongshan\",\"大棟山\",\"大棟山\",\"大棟山\",\"大棟山\",\"大栋山\"\n\"C1O880\",\"關山\",\"Guanshan\",\"関山\",\"關山\",\"關山\",\"關山\",\"关山\"\n\"C1O921\",\"楠西\",\"Nanxi\",\"楠西\",\"楠西\",\"楠西\",\"楠西\",\"楠西\"\n\"C1O940\",\"東山服務區\",\"Dongshan Service Area\",\"東山服務區\",\"東山服務區\",\"東山服務區\",\"東山服務區\",\"东山服务区\"\n\"C1R110\",\"口社\",\"Gusia\",\"口社\",\"口社\",\"口社\",\"口社\",\"口社\"\n\"C1R120\",\"上德文\",\"Shangdewun\",\"上德文\",\"上德文\",\"上德文\",\"上德文\",\"上德文\"\n\"C1R250\",\"力里\",\"Lili\",\"力里\",\"力里\",\"力里\",\"力里\",\"力里\"\n\"C1R290\",\"石門山\",\"Shihmenshan\",\"石門山\",\"石門山\",\"石門山\",\"石門山\",\"石门山\"\n\"C1R610\",\"西大武山\",\"Xidawushan\",\"西大武山\",\"西大武山\",\"西大武山\",\"西大武山\",\"西大武山\"\n\"C1R630\",\"龍泉\",\"Longquan\",\"龍泉\",\"龍泉\",\"龍泉\",\"龍泉\",\"龙泉\"\n\"C1S670\",\"摩天\",\"Motian\",\"摩天\",\"摩天\",\"摩天\",\"摩天\",\"摩天\"\n\"C1S800\",\"華源\",\"Huayuan\",\"華源\",\"華源\",\"華源\",\"華源\",\"华源\"\n\"C1S820\",\"金峰\",\"Jinfeng\",\"金峰\",\"金峰\",\"金峰\",\"金峰\",\"金峰\"\n\"C1S850\",\"豐南\",\"Funan\",\"豐南\",\"豐南\",\"豐南\",\"豐南\",\"丰南\"\n\"C1S860\",\"利嘉\",\"Lichai\",\"利嘉\",\"利嘉\",\"利嘉\",\"利嘉\",\"利嘉\"\n\"C1S870\",\"南美山\",\"Nanmaisan\",\"南美山\",\"南美山\",\"南美山\",\"南美山\",\"南美山\"\n\"C1S880\",\"壽卡\",\"Shouka\",\"壽卡\",\"壽卡\",\"壽卡\",\"壽卡\",\"寿卡\"\n\"C1SA50\",\"利嘉林道\",\"Lijialindao\",\"利嘉林道\",\"利嘉林道\",\"利嘉林道\",\"利嘉林道\",\"利嘉林道\"\n\"C1SA70\",\"都蘭\",\"Dulan\",\"都蘭\",\"都蘭\",\"都蘭\",\"都蘭\",\"都兰\"\n\"C1T800\",\"洛韶\",\"Luoshao\",\"洛韶\",\"洛韶\",\"洛韶\",\"洛韶\",\"洛韶\"\n\"C1T810\",\"慈恩\",\"Ci-En\",\"慈恩\",\"慈恩\",\"慈恩\",\"慈恩\",\"慈恩\"\n\"C1T830\",\"布洛灣\",\"Buluowan\",\"布洛灣\",\"布洛灣\",\"布洛灣\",\"布洛灣\",\"布洛湾\"\n\"C1T920\",\"中興\",\"Zhongxing\",\"中興\",\"中興\",\"中興\",\"中興\",\"中兴\"\n\"C1T940\",\"大觀\",\"Daguan\",\"大觀\",\"大觀\",\"大觀\",\"大觀\",\"大观\"\n\"C1T950\",\"太安\",\"Tai-An\",\"太安\",\"太安\",\"太安\",\"太安\",\"太安\"\n\"C1T970\",\"大農\",\"Danong\",\"大農\",\"大農\",\"大農\",\"大農\",\"大农\"\n\"C1T980\",\"龍澗\",\"Longjian\",\"龍澗\",\"龍澗\",\"龍澗\",\"龍澗\",\"龙涧\"\n\"C1T990\",\"高寮\",\"Gaoliao\",\"高寮\",\"高寮\",\"高寮\",\"高寮\",\"高寮\"\n\"C1TA00\",\"太魯閣\",\"Taroko\",\"太魯閣\",\"太魯閣\",\"太魯閣\",\"太魯閣\",\"太鲁阁\"\n\"C1U501\",\"牛鬥\",\"Nioudou\",\"牛鬥\",\"牛鬥\",\"牛鬥\",\"牛鬥\",\"牛斗\"\n\"C1U670\",\"寒溪\",\"Hanxi\",\"寒溪\",\"寒溪\",\"寒溪\",\"寒溪\",\"寒溪\"\n\"C1U840\",\"東澳嶺\",\"Dongaoling\",\"東澳嶺\",\"東澳嶺\",\"東澳嶺\",\"東澳嶺\",\"东澳岭\"\n\"C1U850\",\"觀音海岸\",\"Guanyin Coast\",\"觀音海岸\",\"觀音海岸\",\"觀音海岸\",\"觀音海岸\",\"观音海岸\"\n\"C1U920\",\"思源\",\"Siyuan\",\"思源\",\"思源\",\"思源\",\"思源\",\"思源\"\n\"C1U930\",\"粉鳥林\",\"Fenniaolin\",\"粉鳥林\",\"粉鳥林\",\"粉鳥林\",\"粉鳥林\",\"粉鸟林\"\n\"C1V160\",\"達卡努瓦\",\"Dakanuwa\",\"達卡努瓦\",\"達卡努瓦\",\"達卡努瓦\",\"達卡努瓦\",\"达卡努瓦\"\n\"C1V170\",\"排雲\",\"Paiyun\",\"排雲\",\"排雲\",\"排雲\",\"排雲\",\"排云\"\n\"C1V190\",\"南天池\",\"Nantianchi\",\"南天池\",\"南天池\",\"南天池\",\"南天池\",\"南天池\"\n\"C1V200\",\"梅山\",\"Meishan\",\"梅山\",\"梅山\",\"梅山\",\"梅山\",\"梅山\"\n\"C1V220\",\"小關山\",\"Xiaoguanshan\",\"小關山\",\"小關山\",\"小關山\",\"小關山\",\"小关山\"\n\"C1V231\",\"高中\",\"Gaozhong\",\"高中\",\"高中\",\"高中\",\"高中\",\"高中\"\n\"C1V300\",\"御油山\",\"Yuyoushan\",\"御油山\",\"御油山\",\"御油山\",\"御油山\",\"御油山\"\n\"C1V340\",\"大津\",\"Dajin\",\"大津\",\"大津\",\"大津\",\"大津\",\"大津\"\n\"C1V390\",\"尖山\",\"Jianshan\",\"尖山\",\"尖山\",\"尖山\",\"尖山\",\"尖山\"\n\"C1V570\",\"吉東\",\"Jiadong\",\"吉東\",\"吉東\",\"吉東\",\"吉東\",\"吉东\"\n\"C1V580\",\"溪南(特生中心)\",\"Xinan\",\"溪南(特生中心)\",\"溪南(特生中心)\",\"溪南(特生中心)\",\"溪南(特生中心)\",\"溪南(特生中心)\"\n\"C1V590\",\"新發\",\"Xinfa\",\"新發\",\"新發\",\"新發\",\"新發\",\"新发\"\n\"C1V600\",\"藤枝\",\"Tengzhi\",\"藤枝\",\"藤枝\",\"藤枝\",\"藤枝\",\"藤枝\"\n\"C1V780\",\"多納林道\",\"Duonalindao\",\"多納林道\",\"多納林道\",\"多納林道\",\"多納林道\",\"多纳林道\"\n\"C1V830\",\"國三S383K\",\"Freeway No. 3 - Rain – S383k\",\"國三S383K\",\"國三S383K\",\"國三S383K\",\"國三S383K\",\"国三S383K\"\n\"C1X040\",\"東原\",\"Dongyuan\",\"東原\",\"東原\",\"東原\",\"東原\",\"东原\"\n\"C1Z030\",\"紅葉\",\"Hongye\",\"紅葉\",\"紅葉\",\"紅葉\",\"紅葉\",\"红叶\"\n\"C1Z040\",\"立山\",\"Lishan\",\"立山\",\"立山\",\"立山\",\"立山\",\"立山\"\n\"C1Z110\",\"三棧\",\"Sanzhan\",\"三棧\",\"三棧\",\"三棧\",\"三棧\",\"三栈\"\n\"C1Z120\",\"壽豐\",\"Shoufeng\",\"寿豊\",\"壽豐\",\"壽豐\",\"壽豐\",\"寿丰\"\n\"C1Z130\",\"銅門\",\"Tongmen\",\"銅門\",\"銅門\",\"銅門\",\"銅門\",\"铜门\"\n\"C1Z140\",\"荖溪\",\"Laoxi\",\"荖溪\",\"荖溪\",\"荖溪\",\"荖溪\",\"荖溪\"\n\"C1Z240\",\"中平林道\",\"Zhongpinglindao\",\"中平林道\",\"中平林道\",\"中平林道\",\"中平林道\",\"中平林道\"\n"
  },
  {
    "path": "assets/translations/zh-Hans.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: zh-CN\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 26\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Chinese Simplified\\n\"\n\"Language: zh_CN\\n\"\n\"PO-Revision-Date: 2026-04-01 07:16\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"正在更新位置\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"正在获取 GPS 位置...\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"接收全部\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"关闭\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"接收类别\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"所在地震度1以上\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"海啸信息、海啸警报\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"只接收海啸警报\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"接收所在地\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"所在地震度4以上\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"音效测试\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"公告\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"发送公告时\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"音效测试是在设备上执行的本地通知，仅用于确认设备在接收通知时是否能正常播放音效。此测试不会向服务器发送任何请求\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"服务器排队中，请稍候…\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"通知\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"设置推送通知及测试通知音效\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"尚未设置所在地\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"请先设置所在地以使用通知功能\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"设置所在地\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"地震预警\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"紧急地震预警\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"地震\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"强震监控仪\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"地震报告\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"震度速报\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"天气\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"雷雨实时信息\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"天气警特报\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"防灾信息\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"海啸\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"海啸信息\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"其他\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"重大\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"海啸警报发布时\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"常规\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"海啸信息发布时\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"太平洋海啸消息(静音通知)\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"太平洋海啸消息发布时\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"强震监视器(普通)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"检测到震动\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"震度速报(普通)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"所在地(乡镇)实测震度 3 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"震度速报(静音通知)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"所在地(乡镇)实测震度 1 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"地震报告(普通)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"所在地(县市)实测震度 3 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"地震报告(静音通知)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"所在地(县市)实测震度 1 以上\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"已更新通知设置\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"更新通知设定失败\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"紧急地震预警(重大)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"最大震度 5 弱以上 且\\n\"\n\"所在地(乡镇)预估震度 4 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"紧急地震预警(普通)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"最大震度 5 弱以上 且\\n\"\n\"所在地(乡镇)预估震度 2 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"紧急地震预警(静音)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"最大震度 5 弱以上 且\\n\"\n\"所在地(乡镇)预估震度 1 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"地震预警(重大)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"所在地(乡镇)预估震度 4 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"地震预警(普通)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"所在地(乡镇)预估震度 2 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"地震预警(静音)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"所在地(乡镇)预估震度 1 以上\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"所在地（乡镇）发布防灾警讯时\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"所在地（乡镇）发布防灾资讯时\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"所在地(乡镇)发布红色警报的\\n\"\n\"气象特别警报\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"所在地(乡镇)发布上述除外颜色信号的\\n\"\n\"气象特别警报\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"所在地(乡镇)发布山区暴雨时\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"所在地(乡镇)发布雷雨即时信息时\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"实验性功能\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"取消\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"单位\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"调整 DPIP 显示数值所使用的单位\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"使用华氏度\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"语言\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"更改 DPIP 的显示语言\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"显示语言\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"系统语言\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"帮助翻译\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"点击这里来帮助我们改进 DPIP 的翻译\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"已翻译 {translated}・已核对 {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"来源语言\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"选择语言\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"无法连接到商店，请稍后再试\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"找不到商品，请稍后再试\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"重新加载\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"正在加载商店商品中\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"订阅制\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/月\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"单次支持\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"恢复购买\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"无法连接至 {store}，请稍后再试。\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"正在恢复您购买的订阅\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"使用条款\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"隐私政策\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"HTTP 代理\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"设置\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"位置\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"所在地\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"设置您的所在地，以接收本地的实时信息\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"界面\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"布局\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"主题\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"更改 DPIP 整体的外观与颜色\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"地图\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"信息\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"更新日志\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"查看 DPIP 的更新记录\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"第三方套件授权\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"赞助我们\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"帮助我们维护服务器的稳定和长久发展\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"调试\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"应用程序版本\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"设备信息\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"复制通知 Token\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"App 日志\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"无法获取通知权限\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"无法获取位置权限\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"无法获取自启动权限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"省电策略\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"无法取得权限\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"自动定位功能需要您允许 DPIP 使用通知权限才能正常运作。请您到应用程序设置中找到并允许“通知”权限后再试一次。\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"自动定位功能需要您允许 DPIP 使用位置权限才能正常运作。请您到应用程序设置中找到并允许“位置”权限后再试一次。\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"自动定位功能需要您永远允许 DPIP 使用位置权限才能正常运作。请您到应用程序设置中找到位置权限设置并选择“永久”后再试一次。\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"自动定位功能需要您一律允许 DPIP 使用位置权限才能正常运作。请您到应用程序设置中找到位置权限设置并选择“始终允许”后再试一次。\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"为了获得更好的自动定位体验，您需要给予“自启动权限”以便让 DPIP 在背景自动设置所在地信息。\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"为了获得更好的自动定位体验，您需要给予“无限制”以便让 DPIP 在背景自动设置所在地信息。\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"自动定位功能需要您允许 DPIP 使用权限才能正常运作。请您到应用程序设置中找到并允许“权限”后再试一次。\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"自动启动\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"为了获得更好的 DPIP 体验，请依照步骤启用自动启动功能，以便让 DPIP 在背景能正常接收资讯以及更新所在地。\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"为了获得更好的 DPIP 体验，请依照步骤关闭省电策略，以便让 DPIP 在背景能正常接收资讯以及更新所在地。\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"始终允许\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"永久\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"自动更新\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"定期更新当前位置\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"自动定位功能会使用您设备上的 GPS，即使 DPIP 处于关闭或未使用状态，也会根据您的地理位置自动更新当前所在位置，提供实时天气与地震信息，助您随时掌握本地最新动态。\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"通知功能已被拒绝，请前往设置允许权限。\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"省电策略被拒绝，请前往设置允许权限。\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"设置所在地时发生错误，请稍后再试。\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"添加地点\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"县市\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"当前位置\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"默认图层\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"自动缩放\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"画面帧率\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"主题模式\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"浅色\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"深色\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"跟随系统主题\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"您所在区域附近正发生强雷雨或降雨，请注意防范，预计持续至 <bold>{time}</bold> 。\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"天气预报(24h)\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"服务区域外，仅在台湾区域可用\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"雷达拼图\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"无数据\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"确定\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"尚未设置\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"湿度\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"风速\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"风向\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"风级\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"气压\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"降雨\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"能见度\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"阵风\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"瞬时风级\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"日照\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"全国•生效中\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"全国•历史\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"所在地•生效中\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"所在地 · 历史\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"地震预警\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"第 {serial} 报\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"{time} 左右，<bold>{location}</bold>附近发生有感地震，预计规模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"{time} 左右，<bold>{location}</bold>附近发生有感地震，预计规模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"所在地预估\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"震波\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \" 秒\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"抵达\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"已更新至 {version}\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"取得天气异常\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"取得历史资料异常\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"再试一次\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"下一步\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"防灾信息平台\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"我们是谁？\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio 是一群大部分由学生组成，平均年龄未满 20 岁、人数超过 15 人的团体。成员来自台湾北中南、日本、韩国、中国的学生。\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"我们的初衷\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"成立初衷是招募一群对计算机及科技有兴趣及能力的同学，后来发展至校外，并逐渐形成现在的样子。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"注意事项\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"任何信息应以中央气象署发布内容为准。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"根据网络状态、服务器状态、应用程序状态、上游数据来源状态等，有收不到信息的可能性，我们会尽力避免此类情况，但不保证一定不会发生。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"强烈震动有概率比通知先抵达用户所在地。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"地震速报为快速计算的结果，可能存在较大误差，应理解并谨慎使用。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"任何不被官方所认可的行为均有可能承担法律风险，请务必遵守相关规范。\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"欢迎使用 DPIP\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"DPIP 是一款由台湾本土团队设计的 App，集成 TREM-Net (台湾即时地震观测网) 的信息及中央气象署数据，提供一个集成、单一且便利的防灾信息应用。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"在重大灾害发生时以通知来传递即时防灾信息\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"使用定位来自动更新所在地设置，提供当地的即时防灾信息\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"允许DPIP在后台持续运行，以便获取实时防灾通知。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"保存\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"权限请求\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"需要用户手动前往设置开启相关权限。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"需要背景位置权限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"为了在后台持续提供实时防灾信息，DPIP需要「始终允许」位置权限。\\n\\n\"\n\"接下来系统将引导您到设置页面，请选择「始终允许」选项。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"稍后\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"前往设置\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"权限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"我们一直和用户站在一起，为用户的隐私而不断努力。\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"地图图层\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"选择要显示的地图图层\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"底图\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"简单\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"监视器\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"报告\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"气象\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"气温\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"降水\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"风向/风速\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"雷电\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"单位：{unit}\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"位于\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"长按设定播放起点\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"目前时间\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"播放起点\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"播放进度\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"5 分钟内对地雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"10 分钟内对地雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"30 分钟内对地雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"60 分钟内对地雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"5 分钟内云间雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"10 分钟内云间雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"30 分钟内云间雷电\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"60 分钟内云间雷电\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"今天\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10 分钟\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1 小时\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3 小时\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6 小时\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12 小时\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24 小时\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2 天\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3 天\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"海外测站\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"实时震度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"地动加速度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"地动速度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"规模 <bold>M{magnitude}</bold>，所在地预估<bold>{intensity}</bold>\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"{countdown}秒后抵达\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"已抵达\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"规模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"目前没有生效中的地震预警\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"近期地震报告\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"更多\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"序号 {number} 显著有感地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"小区域有感地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"地震震级\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"震源深度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"报告详情\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"发震时刻\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"各地震度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"地震报告图\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"震度图\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"峰值地震动加速度图\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"峰值最大地震动速度图\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"错误\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"已解决\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"影响：小\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"影响：中\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"影响：大\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"维护\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"测试\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"变更\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"完成\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"地震相关信息\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"气象相关信息\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"未知\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"目前没有通知\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"通知详情\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"请领导应用程序设定中找到并允许「相片和媒体」权限后在试一次。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"已保存图片\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"保存图片时发生错误\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"０级\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"１级\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"２级\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"３级\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"４级\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"５弱\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"５强\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"６弱\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"６强\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"七级\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"晴\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"晴天有霾\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"晴有霭\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"晴有闪电\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"晴天有雷\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"晴有雾\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"晴天有雨\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"晴天有雨夹雪\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"晴天有大雪\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"晴天有雪丸\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"晴天有冰粒\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"晴有阵雪\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"晴阵雨雪\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"晴天有冰雹\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"晴天有雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"晴天有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"晴天有雷冰雹\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"晴天有强雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"晴天有强雷冰雹\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"局部多云\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"局部多云有霾\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"局部多云有薄雾\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"局部多云有闪电\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"局部多云有雷\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"局部多云有雾\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"局部多云有雨\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"局部多云有雨夹雪\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"局部多云有大雪\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"局部多云有雪丸\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"局部多云有冰丸\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"局部多云有阵雪\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"局部多云有阵雨夹雪\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"局部多云有冰雹\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"局部多云有雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"局部多云有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"局部多云有雷冰雹\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"局部多云有强雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"局部多云有强雷冰雹\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"阴\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"晴有霾\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"晴有霭\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"阴天有闪电\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"晴天伴有雷\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"晴有雾\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"晴有雨\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"阴天有雨夹雪\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"阴天有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"阴天有雪丸\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"阴天有冰丸\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"阴天有阵雪\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"阴天有阵雨夹雪\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"晴有雹\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"阴天有雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"阴天有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"阴天有雷冰雹\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"阴天有强雷阵雨\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"阴天有强雷冰雹\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{city}{cityLevel} {town}{townLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city}{cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town}{townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "assets/translations/zh-Hant.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: zh-TW\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 20\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Chinese Traditional\\n\"\n\"Language: zh_TW\\n\"\n\n#: ./lib/core/service.dart:291\nmsgid \"正在更新位置\"\nmsgstr \"正在更新位置\"\n\n#: ./lib/core/service.dart:292\nmsgid \"取得 GPS 位置中...\"\nmsgstr \"取得 GPS 位置中...\"\n\n#: ./lib/app/settings/notify/page.dart:51\nmsgid \"接收全部\"\nmsgstr \"接收全部\"\n\n#: ./lib/app/settings/notify/page.dart:50\nmsgid \"關閉\"\nmsgstr \"關閉\"\n\n#: ./lib/app/settings/notify/_widgets/eew_notify_section.dart:51\nmsgid \"接收類別\"\nmsgstr \"接收類別\"\n\n#: ./lib/app/settings/notify/page.dart:35\nmsgid \"所在地震度1以上\"\nmsgstr \"所在地震度1以上\"\n\n#: ./lib/app/settings/notify/page.dart:46\nmsgid \"海嘯消息、海嘯警報\"\nmsgstr \"海嘯消息、海嘯警報\"\n\n#: ./lib/app/settings/notify/page.dart:45\nmsgid \"只接收海嘯警報\"\nmsgstr \"只接收海嘯警報\"\n\n#: ./lib/app/settings/notify/page.dart:41\nmsgid \"接收所在地\"\nmsgstr \"接收所在地\"\n\n#: ./lib/app/settings/notify/page.dart:27\nmsgid \"所在地震度4以上\"\nmsgstr \"所在地震度4以上\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:28\nmsgid \"音效測試\"\nmsgstr \"音效測試\"\n\n#: ./lib/route/announcement/announcement.dart:81\nmsgid \"公告\"\nmsgstr \"公告\"\n\n#: ./lib/app/settings/notify/(5.basic)/announcement/page.dart:32\nmsgid \"發送公告時\"\nmsgstr \"發送公告時\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:46\nmsgid \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\nmsgstr \"音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求\"\n\n#: ./lib/app/settings/notify/page.dart:82\nmsgid \"伺服器排隊中，請稍候…\"\nmsgstr \"伺服器排隊中，請稍候…\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:166\nmsgid \"通知\"\nmsgstr \"通知\"\n\n#: ./lib/app/settings/page.dart:182\nmsgid \"推播通知設定與通知音效測試\"\nmsgstr \"推播通知設定與通知音效測試\"\n\n#: ./lib/app/home/_widgets/location_not_set_card.dart:33\nmsgid \"尚未設定所在地\"\nmsgstr \"尚未設定所在地\"\n\n#: ./lib/app/settings/notify/page.dart:201\nmsgid \"請先設定所在地來使用通知功能\"\nmsgstr \"請先設定所在地來使用通知功能\"\n\n#: ./lib/app/settings/notify/page.dart:211\nmsgid \"設定所在地\"\nmsgstr \"設定所在地\"\n\n#: ./lib/app/settings/experimental/page.dart:209\nmsgid \"地震速報\"\nmsgstr \"地震速報\"\n\n#: ./lib/app/settings/notify/page.dart:232\nmsgid \"緊急地震速報\"\nmsgstr \"緊急地震速報\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:113\nmsgid \"地震\"\nmsgstr \"地震\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1579\nmsgid \"強震監視器\"\nmsgstr \"強震監視器\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1302\nmsgid \"地震報告\"\nmsgstr \"地震報告\"\n\n#: ./lib/app/settings/notify/page.dart:290\nmsgid \"震度速報\"\nmsgstr \"震度速報\"\n\n#: ./lib/app/settings/notify/page.dart:304\nmsgid \"天氣\"\nmsgstr \"天氣\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:63\nmsgid \"雷雨即時訊息\"\nmsgstr \"雷雨即時訊息\"\n\n#: ./lib/app/settings/notify/page.dart:334\nmsgid \"天氣警特報\"\nmsgstr \"天氣警特報\"\n\n#: ./lib/app/settings/notify/page.dart:354\nmsgid \"防災資訊\"\nmsgstr \"防災資訊\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:129\nmsgid \"海嘯\"\nmsgstr \"海嘯\"\n\n#: ./lib/app/settings/notify/page.dart:378\nmsgid \"海嘯資訊\"\nmsgstr \"海嘯資訊\"\n\n#: ./lib/app/settings/notify/page.dart:390\nmsgid \"其他\"\nmsgstr \"其他\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:31\nmsgid \"重大\"\nmsgstr \"重大\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:31\nmsgid \"海嘯警報發布時\"\nmsgstr \"海嘯警報發布時\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:37\nmsgid \"一般\"\nmsgstr \"一般\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:37\nmsgid \"海嘯消息發布時\"\nmsgstr \"海嘯消息發布時\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:41\nmsgid \"太平洋海嘯消息(無聲通知)\"\nmsgstr \"太平洋海嘯消息(無聲通知)\"\n\n#: ./lib/app/settings/notify/(4.tsunami)/tsunami/page.dart:42\nmsgid \"太平洋海嘯消息發布時\"\nmsgstr \"太平洋海嘯消息發布時\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:30\nmsgid \"強震監視器(一般)\"\nmsgstr \"強震監視器(一般)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/monitor/page.dart:31\nmsgid \"偵測到晃動\"\nmsgstr \"偵測到晃動\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:30\nmsgid \"震度速報(一般)\"\nmsgstr \"震度速報(一般)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:31\nmsgid \"所在地(鄉鎮)實測震度 3 以上\"\nmsgstr \"所在地(鄉鎮)實測震度 3 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:36\nmsgid \"震度速報(無聲通知)\"\nmsgstr \"震度速報(無聲通知)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/intensity/page.dart:37\nmsgid \"所在地(鄉鎮)實測震度 1 以上\"\nmsgstr \"所在地(鄉鎮)實測震度 1 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:30\nmsgid \"地震報告(一般)\"\nmsgstr \"地震報告(一般)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:31\nmsgid \"所在地(縣市)實測震度 3 以上\"\nmsgstr \"所在地(縣市)實測震度 3 以上\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:36\nmsgid \"地震報告(無聲通知)\"\nmsgstr \"地震報告(無聲通知)\"\n\n#: ./lib/app/settings/notify/(2.earthquake)/report/page.dart:37\nmsgid \"所在地(縣市)實測震度 1 以上\"\nmsgstr \"所在地(縣市)實測震度 1 以上\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:15\nmsgid \"已更新通知設定\"\nmsgstr \"已更新通知設定\"\n\n#: ./lib/app/settings/notify/_lib/utils.dart:22\nmsgid \"更新通知設定失敗\"\nmsgstr \"更新通知設定失敗\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:30\nmsgid \"緊急地震速報(重大)\"\nmsgstr \"緊急地震速報(重大)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:31\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 4 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:36\nmsgid \"緊急地震速報(一般)\"\nmsgstr \"緊急地震速報(一般)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:37\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 2 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:41\nmsgid \"緊急地震速報(無聲)\"\nmsgstr \"緊急地震速報(無聲)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:42\nmsgid \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"\"\n\"最大震度 5 弱以上 且\\n\"\n\"所在地(鄉鎮)預估震度 1 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:46\nmsgid \"地震速報(重大)\"\nmsgstr \"地震速報(重大)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:47\nmsgid \"所在地(鄉鎮)預估震度 4 以上\"\nmsgstr \"所在地(鄉鎮)預估震度 4 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:51\nmsgid \"地震速報(一般)\"\nmsgstr \"地震速報(一般)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:52\nmsgid \"所在地(鄉鎮)預估震度 2 以上\"\nmsgstr \"所在地(鄉鎮)預估震度 2 以上\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:56\nmsgid \"地震速報(無聲)\"\nmsgstr \"地震速報(無聲)\"\n\n#: ./lib/app/settings/notify/(1.eew)/eew/page.dart:57\nmsgid \"所在地(鄉鎮)預估震度 1 以上\"\nmsgstr \"所在地(鄉鎮)預估震度 1 以上\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:32\nmsgid \"所在地(鄉鎮)發布防災警訊時\"\nmsgstr \"所在地(鄉鎮)發布防災警訊時\"\n\n#: ./lib/app/settings/notify/(3.weather)/evacuation/page.dart:38\nmsgid \"所在地(鄉鎮)發布防災資訊時\"\nmsgstr \"所在地(鄉鎮)發布防災資訊時\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:32\nmsgid \"\"\n\"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\"所在地(鄉鎮)發布紅色燈號之\\n\"\n\"天氣警特報\"\n\n#: ./lib/app/settings/notify/(3.weather)/advisory/page.dart:38\nmsgid \"\"\n\"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\nmsgstr \"\"\n\"所在地(鄉鎮)發布上述除外燈號之\\n\"\n\"天氣警特報\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:32\nmsgid \"所在地(鄉鎮)發布山區暴雨時\"\nmsgstr \"所在地(鄉鎮)發布山區暴雨時\"\n\n#: ./lib/app/settings/notify/(3.weather)/thunderstorm/page.dart:38\nmsgid \"所在地(鄉鎮)發布雷雨即時訊息時\"\nmsgstr \"所在地(鄉鎮)發布雷雨即時訊息時\"\n\n#: ./lib/app/settings/experimental/page.dart:197\nmsgid \"啟動時進入強震監視器\"\nmsgstr \"啟動時進入強震監視器\"\n\n#: ./lib/app/settings/experimental/page.dart:57\nmsgid \"地震速報不限制非 CWA 來源\"\nmsgstr \"地震速報不限制非 CWA 來源\"\n\n#: ./lib/app/settings/page.dart:428\nmsgid \"實驗性功能\"\nmsgstr \"實驗性功能\"\n\n#: ./lib/app/settings/page.dart:429\nmsgid \"搶先體驗開發中的新功能\"\nmsgstr \"搶先體驗開發中的新功能\"\n\n#: ./lib/app/settings/experimental/page.dart:154\nmsgid \"注意\"\nmsgstr \"注意\"\n\n#: ./lib/app/settings/experimental/page.dart:162\nmsgid \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\nmsgstr \"這些功能仍在開發中，可能會不穩定或在未來的版本中變更。\"\n\n#: ./lib/app/settings/experimental/page.dart:188\nmsgid \"啟動行為\"\nmsgstr \"啟動行為\"\n\n#: ./lib/app/settings/experimental/page.dart:198\nmsgid \"開啟 App 時直接進入強震監視器地圖\"\nmsgstr \"開啟 App 時直接進入強震監視器地圖\"\n\n#: ./lib/app/settings/experimental/page.dart:218\nmsgid \"不限制非 CWA 來源\"\nmsgstr \"不限制非 CWA 來源\"\n\n#: ./lib/app/settings/experimental/page.dart:219\nmsgid \"顯示所有來源的地震速報資料\"\nmsgstr \"顯示所有來源的地震速報資料\"\n\n#: ./lib/app/settings/experimental/page.dart:280\nmsgid \"啟用實驗性功能\"\nmsgstr \"啟用實驗性功能\"\n\n#: ./lib/app/settings/experimental/page.dart:286\nmsgid \"你即將啟用：\"\nmsgstr \"你即將啟用：\"\n\n#: ./lib/app/settings/experimental/page.dart:317\nmsgid \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\nmsgstr \"此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:78\nmsgid \"取消\"\nmsgstr \"取消\"\n\n#: ./lib/app/settings/layout/page.dart:272\nmsgid \"啟用\"\nmsgstr \"啟用\"\n\n#: ./lib/app/settings/page.dart:151\nmsgid \"單位\"\nmsgstr \"單位\"\n\n#: ./lib/app/settings/page.dart:152\nmsgid \"調整 DPIP 顯示數值時使用的單位\"\nmsgstr \"調整 DPIP 顯示數值時使用的單位\"\n\n#: ./lib/app/settings/unit/page.dart:60\nmsgid \"使用華氏度\"\nmsgstr \"使用華氏度\"\n\n#: ./lib/app/settings/unit/page.dart:61\nmsgid \"切換溫度顯示單位為華氏度 (℉)\"\nmsgstr \"切換溫度顯示單位為華氏度 (℉)\"\n\n#: ./lib/app/settings/page.dart:141\nmsgid \"語言\"\nmsgstr \"語言\"\n\n#: ./lib/app/settings/page.dart:142\nmsgid \"調整 DPIP 的顯示語言\"\nmsgstr \"調整 DPIP 的顯示語言\"\n\n#: ./lib/app/settings/locale/page.dart:41\nmsgid \"顯示語言\"\nmsgstr \"顯示語言\"\n\n#: ./lib/app/settings/locale/page.dart:42\nmsgid \"系統語言\"\nmsgstr \"系統語言\"\n\n#: ./lib/app/settings/locale/page.dart:52\nmsgid \"協助翻譯\"\nmsgstr \"協助翻譯\"\n\n#: ./lib/app/settings/locale/page.dart:53\nmsgid \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\nmsgstr \"點擊這裡來幫助我們改進 DPIP 的翻譯\"\n\n#: ./lib/app/settings/locale/select/page.dart:116\nmsgid \"已翻譯 {translated}・已校對 {approved}\"\nmsgstr \"已翻譯 {translated}・已校對 {approved}\"\n\n#: ./lib/app/settings/locale/select/page.dart:129\nmsgid \"來源語言\"\nmsgstr \"來源語言\"\n\n#: ./lib/app/settings/locale/select/page.dart:163\nmsgid \"選擇語言\"\nmsgstr \"選擇語言\"\n\n#: ./lib/app/settings/donate/page.dart:52\nmsgid \"無法連線至商店，請稍後再試\"\nmsgstr \"無法連線至商店，請稍後再試\"\n\n#: ./lib/app/settings/donate/page.dart:59\nmsgid \"找不到商品，請稍候再試\"\nmsgstr \"找不到商品，請稍候再試\"\n\n#: ./lib/app/map/_lib/managers/report.dart:521\nmsgid \"重新載入\"\nmsgstr \"重新載入\"\n\n#: ./lib/app/settings/donate/page.dart:171\nmsgid \"正在載入商店物品中\"\nmsgstr \"正在載入商店物品中\"\n\n#: ./lib/app/settings/donate/page.dart:225\nmsgid \"支持 DPIP\"\nmsgstr \"支持 DPIP\"\n\n#: ./lib/app/settings/donate/page.dart:233\nmsgid \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\nmsgstr \"DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。\"\n\n#: ./lib/app/settings/donate/page.dart:260\nmsgid \"訂閱制\"\nmsgstr \"訂閱制\"\n\n#: ./lib/app/settings/donate/page.dart:276\nmsgid \"推薦\"\nmsgstr \"推薦\"\n\n#: ./lib/app/settings/donate/page.dart:443\nmsgid \"{price}/月\"\nmsgstr \"{price}/月\"\n\n#: ./lib/app/settings/donate/page.dart:475\nmsgid \"單次支援\"\nmsgstr \"單次支援\"\n\n#: ./lib/app/settings/donate/page.dart:615\nmsgid \"恢復購買\"\nmsgstr \"恢復購買\"\n\n#: ./lib/app/settings/donate/page.dart:628\nmsgid \"無法連線至 {store}，請稍後再試。\"\nmsgstr \"無法連線至 {store}，請稍後再試。\"\n\n#: ./lib/app/settings/donate/page.dart:639\nmsgid \"正在恢復您購買的訂閱\"\nmsgstr \"正在恢復您購買的訂閱\"\n\n#: ./lib/app/settings/donate/page.dart:644\nmsgid \"使用條款\"\nmsgstr \"使用條款\"\n\n#: ./lib/app/settings/donate/page.dart:648\nmsgid \"隱私權政策\"\nmsgstr \"隱私權政策\"\n\n#: ./lib/app/settings/proxy/page.dart:51\nmsgid \"設定已儲存\"\nmsgstr \"設定已儲存\"\n\n#: ./lib/app/settings/page.dart:200\nmsgid \"HTTP 代理\"\nmsgstr \"HTTP 代理\"\n\n#: ./lib/app/settings/page.dart:201\nmsgid \"調整 HTTP 代理伺服器設定\"\nmsgstr \"調整 HTTP 代理伺服器設定\"\n\n#: ./lib/app/settings/proxy/page.dart:75\nmsgid \"啟用代理\"\nmsgstr \"啟用代理\"\n\n#: ./lib/app/settings/proxy/page.dart:76\nmsgid \"透過代理伺服器發送所有網路請求\"\nmsgstr \"透過代理伺服器發送所有網路請求\"\n\n#: ./lib/app/settings/proxy/page.dart:88\nmsgid \"代理主機\"\nmsgstr \"代理主機\"\n\n#: ./lib/app/settings/proxy/page.dart:102\nmsgid \"代理端口\"\nmsgstr \"代理端口\"\n\n#: ./lib/app/settings/proxy/page.dart:117\nmsgid \"設定儲存後，需要重新啟動應用程式才能生效\"\nmsgstr \"設定儲存後，需要重新啟動應用程式才能生效\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"設定\"\nmsgstr \"設定\"\n\n#: ./lib/app/settings/page.dart:71\nmsgid \"自訂你的 DPIP 使用體驗\"\nmsgstr \"自訂你的 DPIP 使用體驗\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:174\nmsgid \"位置\"\nmsgstr \"位置\"\n\n#: ./lib/app/settings/location/page.dart:417\nmsgid \"所在地\"\nmsgstr \"所在地\"\n\n#: ./lib/app/settings/location/page.dart:241\nmsgid \"設定你的所在地來接收當地的即時資訊\"\nmsgstr \"設定你的所在地來接收當地的即時資訊\"\n\n#: ./lib/app/settings/page.dart:113\nmsgid \"介面\"\nmsgstr \"介面\"\n\n#: ./lib/app/settings/layout/page.dart:47\nmsgid \"版面\"\nmsgstr \"版面\"\n\n#: ./lib/app/settings/layout/page.dart:48\nmsgid \"調整首頁的版面樣式\"\nmsgstr \"調整首頁的版面樣式\"\n\n#: ./lib/app/settings/theme/page.dart:25\nmsgid \"主題\"\nmsgstr \"主題\"\n\n#: ./lib/app/settings/theme/page.dart:26\nmsgid \"調整 DPIP 整體的外觀與顏色\"\nmsgstr \"調整 DPIP 整體的外觀與顏色\"\n\n#: ./lib/app/settings/map/page.dart:49\nmsgid \"地圖\"\nmsgstr \"地圖\"\n\n#: ./lib/app/settings/map/page.dart:50\nmsgid \"調整地圖的顯示樣式\"\nmsgstr \"調整地圖的顯示樣式\"\n\n#: ./lib/app/settings/page.dart:191\nmsgid \"網路\"\nmsgstr \"網路\"\n\n#: ./lib/app/settings/page.dart:210\nmsgid \"資訊\"\nmsgstr \"資訊\"\n\n#: ./lib/app/changelog/page.dart:54\nmsgid \"更新日誌\"\nmsgstr \"更新日誌\"\n\n#: ./lib/app/settings/page.dart:230\nmsgid \"瀏覽 DPIP 的歷次更新紀錄\"\nmsgstr \"瀏覽 DPIP 的歷次更新紀錄\"\n\n#: ./lib/app/settings/page.dart:240\nmsgid \"第三方套件授權\"\nmsgstr \"第三方套件授權\"\n\n#: ./lib/app/settings/page.dart:241\nmsgid \"DPIP 的實現歸功於開放原始碼\"\nmsgstr \"DPIP 的實現歸功於開放原始碼\"\n\n#: ./lib/app/settings/page.dart:264\nmsgid \"贊助我們\"\nmsgstr \"贊助我們\"\n\n#: ./lib/app/settings/page.dart:267\nmsgid \"幫助我們維護伺服器的穩定和長久發展\"\nmsgstr \"幫助我們維護伺服器的穩定和長久發展\"\n\n#: ./lib/app/settings/page.dart:343\nmsgid \"下載\"\nmsgstr \"下載\"\n\n#: ./lib/app/settings/page.dart:383\nmsgid \"除錯\"\nmsgstr \"除錯\"\n\n#: ./lib/app/settings/page.dart:391\nmsgid \"應用程式版本\"\nmsgstr \"應用程式版本\"\n\n#: ./lib/app/settings/page.dart:400\nmsgid \"裝置資訊\"\nmsgstr \"裝置資訊\"\n\n#: ./lib/app/settings/page.dart:409\nmsgid \"複製通知 Token\"\nmsgstr \"複製通知 Token\"\n\n#: ./lib/app/debug/logs/page.dart:16\nmsgid \"App 日誌\"\nmsgstr \"App 日誌\"\n\n#: ./lib/app/settings/page.dart:463\nmsgid \"任何資訊應以中央氣象署發布之內容為準\"\nmsgstr \"任何資訊應以中央氣象署發布之內容為準\"\n\n#: ./lib/app/settings/location/page.dart:74\nmsgid \"無法取得通知權限\"\nmsgstr \"無法取得通知權限\"\n\n#: ./lib/app/settings/location/page.dart:76\nmsgid \"無法取得位置權限\"\nmsgstr \"無法取得位置權限\"\n\n#: ./lib/app/settings/location/page.dart:77\nmsgid \"無法取得自啟動權限\"\nmsgstr \"無法取得自啟動權限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:180\nmsgid \"省電策略\"\nmsgstr \"省電策略\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:71\nmsgid \"無法取得權限\"\nmsgstr \"無法取得權限\"\n\n#: ./lib/app/settings/location/page.dart:84\nmsgid \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\nmsgstr \"自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。\"\n\n#: ./lib/app/settings/location/page.dart:86\nmsgid \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\nmsgstr \"自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。\"\n\n#: ./lib/app/settings/location/page.dart:89\nmsgid \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\nmsgstr \"自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。\"\n\n#: ./lib/app/settings/location/page.dart:91\nmsgid \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\nmsgstr \"自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。\"\n\n#: ./lib/app/settings/location/page.dart:93\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。\"\n\n#: ./lib/app/settings/location/page.dart:95\nmsgid \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\nmsgstr \"為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。\"\n\n#: ./lib/app/settings/location/page.dart:96\nmsgid \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\nmsgstr \"自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。\"\n\n#: ./lib/app/settings/location/page.dart:174\nmsgid \"自動啟動\"\nmsgstr \"自動啟動\"\n\n#: ./lib/app/settings/location/page.dart:175\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\n\n#: ./lib/app/settings/location/page.dart:199\nmsgid \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\nmsgstr \"為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"一律允許\"\nmsgstr \"一律允許\"\n\n#: ./lib/app/settings/location/page.dart:233\nmsgid \"永遠\"\nmsgstr \"永遠\"\n\n#: ./lib/app/settings/location/page.dart:253\nmsgid \"自動更新\"\nmsgstr \"自動更新\"\n\n#: ./lib/app/settings/location/page.dart:254\nmsgid \"定期更新目前的所在地\"\nmsgstr \"定期更新目前的所在地\"\n\n#: ./lib/app/settings/location/page.dart:263\nmsgid \"\"\n\"自動定位功能將使用您的裝置上的 GPS，即使 DPIP \"\n\"關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\nmsgstr \"\"\n\"自動定位功能將使用您的裝置上的 GPS，即使 DPIP \"\n\"關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。\"\n\n#: ./lib/app/settings/location/page.dart:334\nmsgid \"通知功能已被拒絕，請移至設定允許權限。\"\nmsgstr \"通知功能已被拒絕，請移至設定允許權限。\"\n\n#: ./lib/app/settings/location/page.dart:395\nmsgid \"省電策略已被拒絕，請移至設定允許權限。\"\nmsgstr \"省電策略已被拒絕，請移至設定允許權限。\"\n\n#: ./lib/app/settings/location/select/[city]/page.dart:98\nmsgid \"設定所在地時發生錯誤，請稍候再試一次。\"\nmsgstr \"設定所在地時發生錯誤，請稍候再試一次。\"\n\n#: ./lib/app/home/_widgets/location_button.dart:233\nmsgid \"新增地點\"\nmsgstr \"新增地點\"\n\n#: ./lib/app/settings/location/select/page.dart:33\nmsgid \"縣市\"\nmsgstr \"縣市\"\n\n#: ./lib/app/settings/location/select/page.dart:44\nmsgid \"目前所在地\"\nmsgstr \"目前所在地\"\n\n#: ./lib/app/settings/layout/page.dart:56\nmsgid \"拖曳調整順序\"\nmsgstr \"拖曳調整順序\"\n\n#: ./lib/app/settings/layout/page.dart:100\nmsgid \"已停用\"\nmsgstr \"已停用\"\n\n#: ./lib/app/settings/layout/page.dart:135\nmsgid \"所有區塊皆已啟用\"\nmsgstr \"所有區塊皆已啟用\"\n\n#: ./lib/app/settings/layout/page.dart:202\nmsgid \"停用\"\nmsgstr \"停用\"\n\n#: ./lib/app/settings/map/page.dart:61\nmsgid \"初始圖層\"\nmsgstr \"初始圖層\"\n\n#: ./lib/app/settings/map/page.dart:62\nmsgid \"調整地圖的底圖以及初始顯示的圖層\"\nmsgstr \"調整地圖的底圖以及初始顯示的圖層\"\n\n#: ./lib/app/settings/map/page.dart:74\nmsgid \"自動縮放\"\nmsgstr \"自動縮放\"\n\n#: ./lib/app/settings/map/page.dart:75\nmsgid \"接收到檢知時自動縮放地圖\"\nmsgstr \"接收到檢知時自動縮放地圖\"\n\n#: ./lib/app/settings/map/page.dart:101\nmsgid \"動畫幀率\"\nmsgstr \"動畫幀率\"\n\n#: ./lib/app/settings/map/page.dart:102\nmsgid \"調整強震監視器震波模擬動畫的流暢度\"\nmsgstr \"調整強震監視器震波模擬動畫的流暢度\"\n\n#: ./lib/app/settings/map/page.dart:136\nmsgid \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\nmsgstr \"過高的動畫幀率可能會造成卡頓或裝置發熱\"\n\n#: ./lib/app/settings/theme/page.dart:46\nmsgid \"主題模式\"\nmsgstr \"主題模式\"\n\n#: ./lib/app/settings/theme/mode/page.dart:63\nmsgid \"淺色\"\nmsgstr \"淺色\"\n\n#: ./lib/app/settings/theme/mode/page.dart:64\nmsgid \"深色\"\nmsgstr \"深色\"\n\n#: ./lib/app/settings/theme/mode/page.dart:59\nmsgid \"跟隨系統主題\"\nmsgstr \"跟隨系統主題\"\n\n#: ./lib/app/settings/theme/color/page.dart:22\nmsgid \"主題色彩\"\nmsgstr \"主題色彩\"\n\n#: ./lib/app/settings/theme/color/page.dart:43\nmsgid \"使用系統配色\"\nmsgstr \"使用系統配色\"\n\n#: ./lib/app/settings/theme/color/page.dart:62\nmsgid \"自訂\"\nmsgstr \"自訂\"\n\n#: ./lib/app/settings/theme/color/page.dart:72\nmsgid \"自訂色彩\"\nmsgstr \"自訂色彩\"\n\n#: ./lib/app/home/_widgets/thunderstorm_card.dart:84\nmsgid \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\nmsgstr \"您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。\"\n\n#: ./lib/app/home/_widgets/forecast_card.dart:59\nmsgid \"天氣預報(24h)\"\nmsgstr \"天氣預報(24h)\"\n\n#: ./lib/app/home/_widgets/location_out_of_service.dart:28\nmsgid \"服務區域外，僅在臺灣各地可用\"\nmsgstr \"服務區域外，僅在臺灣各地可用\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:587\nmsgid \"雷達回波\"\nmsgstr \"雷達回波\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:497\nmsgid \"無資料\"\nmsgstr \"無資料\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:372\nmsgid \"{wind}級 {Desc}\"\nmsgstr \"{wind}級 {Desc}\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:389\nmsgid \"陣風 {speed} m/s\"\nmsgstr \"陣風 {speed} m/s\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:471\nmsgid \"無法測量\"\nmsgstr \"無法測量\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:492\nmsgid \"指北針不可靠\"\nmsgstr \"指北針不可靠\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:494\nmsgid \"指北針準確度下降\"\nmsgstr \"指北針準確度下降\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:495\nmsgid \"指北針正常\"\nmsgstr \"指北針正常\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:502\nmsgid \"方向精確度\"\nmsgstr \"方向精確度\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:521\nmsgid \"正常範圍：±0-15°\"\nmsgstr \"正常範圍：±0-15°\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:538\nmsgid \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\nmsgstr \"附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。\"\n\n#: ./lib/app/home/_widgets/wind_card.dart:539\nmsgid \"附近可能有磁場干擾，指北針方向可能有偏差。\"\nmsgstr \"附近可能有磁場干擾，指北針方向可能有偏差。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:145\nmsgid \"確定\"\nmsgstr \"確定\"\n\n#: ./lib/app/home/_widgets/location_button.dart:29\nmsgid \"尚未設定\"\nmsgstr \"尚未設定\"\n\n#: ./lib/app/home/_widgets/location_button.dart:138\nmsgid \"切換區域\"\nmsgstr \"切換區域\"\n\n#: ./lib/app/home/_widgets/location_button.dart:144\nmsgid \"位置設定\"\nmsgstr \"位置設定\"\n\n#: ./lib/app/home/_widgets/location_button.dart:195\nmsgid \"快速切換\"\nmsgstr \"快速切換\"\n\n#: ./lib/app/home/_widgets/location_button.dart:240\nmsgid \"選擇縣市\"\nmsgstr \"選擇縣市\"\n\n#: ./lib/app/home/_widgets/location_button.dart:250\nmsgid \"目前選擇\"\nmsgstr \"目前選擇\"\n\n#: ./lib/app/home/page.dart:888\nmsgid \"濕度\"\nmsgstr \"濕度\"\n\n#: ./lib/app/home/page.dart:916\nmsgid \"風速\"\nmsgstr \"風速\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:181\nmsgid \"風向\"\nmsgstr \"風向\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:190\nmsgid \"風級\"\nmsgstr \"風級\"\n\n#: ./lib/app/home/page.dart:895\nmsgid \"氣壓\"\nmsgstr \"氣壓\"\n\n#: ./lib/app/home/page.dart:902\nmsgid \"降雨\"\nmsgstr \"降雨\"\n\n#: ./lib/app/home/page.dart:909\nmsgid \"能見度\"\nmsgstr \"能見度\"\n\n#: ./lib/app/home/page.dart:923\nmsgid \"陣風\"\nmsgstr \"陣風\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:233\nmsgid \"陣風級\"\nmsgstr \"陣風級\"\n\n#: ./lib/app/home/_widgets/weather_header.dart:241\nmsgid \"日照\"\nmsgstr \"日照\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:142\nmsgid \"體感 {feelsLike}°\"\nmsgstr \"體感 {feelsLike}°\"\n\n#: ./lib/app/home/_widgets/hero_weather.dart:185\nmsgid \"無天氣資料\"\nmsgstr \"無天氣資料\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:14\nmsgid \"全國 · 生效中\"\nmsgstr \"全國 · 生效中\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:16\nmsgid \"全國 · 歷史\"\nmsgstr \"全國 · 歷史\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:18\nmsgid \"所在地 · 生效中\"\nmsgstr \"所在地 · 生效中\"\n\n#: ./lib/app/home/_widgets/mode_toggle_button.dart:20\nmsgid \"所在地 · 歷史\"\nmsgstr \"所在地 · 歷史\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1258\nmsgid \"EEW\"\nmsgstr \"EEW\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1397\nmsgid \"第 {serial} 報\"\nmsgstr \"第 {serial} 報\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1415\nmsgid \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\nmsgstr \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1423\nmsgid \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\nmsgstr \"\"\n\"{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 \"\n\"<bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1469\nmsgid \"所在地預估\"\nmsgstr \"所在地預估\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1505\nmsgid \"震波\"\nmsgstr \"震波\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1527\nmsgid \" 秒\"\nmsgstr \" 秒\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1544\nmsgid \"抵達\"\nmsgstr \"抵達\"\n\n#: ./lib/app/home/page.dart:195\nmsgid \"已更新至 {version}\"\nmsgstr \"已更新至 {version}\"\n\n#: ./lib/utils/weather_icon.dart:284\nmsgid \"取得天氣異常\"\nmsgstr \"取得天氣異常\"\n\n#: ./lib/app/home/page.dart:366\nmsgid \"取得歷史資訊異常\"\nmsgstr \"取得歷史資訊異常\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"上午\"\nmsgstr \"上午\"\n\n#: ./lib/app/home/page.dart:777\nmsgid \"下午\"\nmsgstr \"下午\"\n\n#: ./lib/app/changelog/page.dart:76\nmsgid \"發生錯誤\"\nmsgstr \"發生錯誤\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:85\nmsgid \"再試一次\"\nmsgstr \"再試一次\"\n\n#: ./lib/app/changelog/page.dart:203\nmsgid \"目前版本\"\nmsgstr \"目前版本\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:403\nmsgid \"下一步\"\nmsgstr \"下一步\"\n\n#: ./lib/app/welcome/1-about/page.dart:68\nmsgid \"防災資訊平台\"\nmsgstr \"防災資訊平台\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:93\nmsgid \"我們是誰？\"\nmsgstr \"我們是誰？\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:100\nmsgid \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\nmsgstr \"ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:106\nmsgid \"我們的初衷\"\nmsgstr \"我們的初衷\"\n\n#: ./lib/app/welcome/2-exptech/page.dart:113\nmsgid \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\nmsgstr \"成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:45\nmsgid \"注意事項\"\nmsgstr \"注意事項\"\n\n#: ./lib/app/welcome/3-notice/page.dart:65\nmsgid \"任何資訊應以中央氣象署發布之內容為準。\"\nmsgstr \"任何資訊應以中央氣象署發布之內容為準。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:82\nmsgid \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\nmsgstr \"根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:97\nmsgid \"強烈搖晃有機率比通知早抵達使用者所在地。\"\nmsgstr \"強烈搖晃有機率比通知早抵達使用者所在地。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:111\nmsgid \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\nmsgstr \"地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。\"\n\n#: ./lib/app/welcome/3-notice/page.dart:125\nmsgid \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\nmsgstr \"任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。\"\n\n#: ./lib/app/welcome/1-about/page.dart:46\nmsgid \"歡迎使用 DPIP\"\nmsgstr \"歡迎使用 DPIP\"\n\n#: ./lib/app/welcome/1-about/page.dart:91\nmsgid \"\"\n\"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) \"\n\"之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\nmsgstr \"\"\n\"DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) \"\n\"之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:167\nmsgid \"在重大災害發生時以通知來傳遞即時防災資訊\"\nmsgstr \"在重大災害發生時以通知來傳遞即時防災資訊\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:175\nmsgid \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\nmsgstr \"使用定位來自動更新所在地設定，提供當地的即時防災資訊\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:181\nmsgid \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\nmsgstr \"允許 DPIP 在背景中持續運行，以便即時防災通知資訊。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:255\nmsgid \"儲存\"\nmsgstr \"儲存\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:188\nmsgid \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\nmsgstr \"用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:352\nmsgid \"權限請求\"\nmsgstr \"權限請求\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:353\nmsgid \"需要使用者手動到設定開啟相關權限。\"\nmsgstr \"需要使用者手動到設定開啟相關權限。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:376\nmsgid \"需要背景位置權限\"\nmsgstr \"需要背景位置權限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:378\nmsgid \"\"\n\"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\"\n\"\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\nmsgstr \"\"\n\"為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\"\n\"\\n\"\n\"接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:384\nmsgid \"稍後\"\nmsgstr \"稍後\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:388\nmsgid \"前往設定\"\nmsgstr \"前往設定\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:426\nmsgid \"權限\"\nmsgstr \"權限\"\n\n#: ./lib/app/welcome/4-permissions/page.dart:439\nmsgid \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\nmsgstr \"我們一直和使用者站在一起，為使用者的隱私而不斷努力。\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:84\nmsgid \"地圖圖層\"\nmsgstr \"地圖圖層\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:85\nmsgid \"選擇要顯示的地圖圖層\"\nmsgstr \"選擇要顯示的地圖圖層\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:91\nmsgid \"底圖\"\nmsgstr \"底圖\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:97\nmsgid \"簡單\"\nmsgstr \"簡單\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:119\nmsgid \"監視器\"\nmsgstr \"監視器\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:124\nmsgid \"報告\"\nmsgstr \"報告\"\n\n#: ./lib/app/map/_widgets/layer_toggle_sheet.dart:135\nmsgid \"氣象\"\nmsgstr \"氣象\"\n\n#: ./lib/app/map/_lib/managers/temperature.dart:453\nmsgid \"氣溫\"\nmsgstr \"氣溫\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:577\nmsgid \"降水\"\nmsgstr \"降水\"\n\n#: ./lib/app/map/_lib/managers/wind.dart:320\nmsgid \"風向/風速\"\nmsgstr \"風向/風速\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:294\nmsgid \"閃電\"\nmsgstr \"閃電\"\n\n#: ./lib/app/map/_widgets/map_legend.dart:202\nmsgid \"單位：{unit}\"\nmsgstr \"單位：{unit}\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:485\nmsgid \"近期無海嘯資訊\"\nmsgstr \"近期無海嘯資訊\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:486\nmsgid \"海嘯警報\"\nmsgstr \"海嘯警報\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:496\nmsgid \"{id}號 第{serial}報\"\nmsgstr \"{id}號 第{serial}報\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:551\nmsgid \"發布\"\nmsgstr \"發布\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:553\nmsgid \"更新\"\nmsgstr \"更新\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:554\nmsgid \"解除\"\nmsgstr \"解除\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:607\nmsgid \"預估海嘯到達時間及波高\"\nmsgstr \"預估海嘯到達時間及波高\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:626\nmsgid \"各地觀測到的海嘯\"\nmsgstr \"各地觀測到的海嘯\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:641\nmsgid \"地震資訊\"\nmsgstr \"地震資訊\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:654\nmsgid \"發生時間\"\nmsgstr \"發生時間\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1072\nmsgid \"位於\"\nmsgstr \"位於\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:736\nmsgid \"規模\"\nmsgstr \"規模\"\n\n#: ./lib/app/map/_lib/managers/tsunami.dart:765\nmsgid \"深度\"\nmsgstr \"深度\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:744\nmsgid \"長按設定播放起點\"\nmsgstr \"長按設定播放起點\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:760\nmsgid \"目前時間\"\nmsgstr \"目前時間\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:765\nmsgid \"播放起點\"\nmsgstr \"播放起點\"\n\n#: ./lib/app/map/_lib/managers/radar.dart:1099\nmsgid \"播放進度\"\nmsgstr \"播放進度\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:393\nmsgid \"5 分鐘內對地閃電\"\nmsgstr \"5 分鐘內對地閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:401\nmsgid \"10 分鐘內對地閃電\"\nmsgstr \"10 分鐘內對地閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:409\nmsgid \"30 分鐘內對地閃電\"\nmsgstr \"30 分鐘內對地閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:417\nmsgid \"60 分鐘內對地閃電\"\nmsgstr \"60 分鐘內對地閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:425\nmsgid \"5 分鐘內雲間閃電\"\nmsgstr \"5 分鐘內雲間閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:433\nmsgid \"10 分鐘內雲間閃電\"\nmsgstr \"10 分鐘內雲間閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:441\nmsgid \"30 分鐘內雲間閃電\"\nmsgstr \"30 分鐘內雲間閃電\"\n\n#: ./lib/app/map/_lib/managers/lightning.dart:449\nmsgid \"60 分鐘內雲間閃電\"\nmsgstr \"60 分鐘內雲間閃電\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:396\nmsgid \"今日\"\nmsgstr \"今日\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:397\nmsgid \"10 分鐘\"\nmsgstr \"10 分鐘\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:398\nmsgid \"1 小時\"\nmsgstr \"1 小時\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:399\nmsgid \"3 小時\"\nmsgstr \"3 小時\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:400\nmsgid \"6 小時\"\nmsgstr \"6 小時\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:401\nmsgid \"12 小時\"\nmsgstr \"12 小時\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:402\nmsgid \"24 小時\"\nmsgstr \"24 小時\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:403\nmsgid \"2 天\"\nmsgstr \"2 天\"\n\n#: ./lib/app/map/_lib/managers/precipitation.dart:404\nmsgid \"3 天\"\nmsgstr \"3 天\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:474\nmsgid \"海外測站\"\nmsgstr \"海外測站\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:494\nmsgid \"即時震度：\"\nmsgstr \"即時震度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:517\nmsgid \"地動加速度：\"\nmsgstr \"地動加速度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:541\nmsgid \"地動速度：\"\nmsgstr \"地動速度：\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1331\nmsgid \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\nmsgstr \"規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1349\nmsgid \"{countdown}秒後抵達\"\nmsgstr \"{countdown}秒後抵達\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1352\nmsgid \"已抵達\"\nmsgstr \"已抵達\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1364\nmsgid \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\nmsgstr \"規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里\"\n\n#: ./lib/app/map/_lib/managers/monitor.dart:1591\nmsgid \"目前沒有生效中的地震速報\"\nmsgstr \"目前沒有生效中的地震速報\"\n\n#: ./lib/app/map/_lib/managers/report.dart:516\nmsgid \"CWA 正在製圖中\"\nmsgstr \"CWA 正在製圖中\"\n\n#: ./lib/app/map/_lib/managers/report.dart:639\nmsgid \"近期的地震報告\"\nmsgstr \"近期的地震報告\"\n\n#: ./lib/app/map/_lib/managers/report.dart:647\nmsgid \"更多\"\nmsgstr \"更多\"\n\n#: ./lib/app/map/_lib/managers/report.dart:995\nmsgid \"編號 {number} 顯著有感地震\"\nmsgstr \"編號 {number} 顯著有感地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:998\nmsgid \"小區域有感地震\"\nmsgstr \"小區域有感地震\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1083\nmsgid \"地震規模\"\nmsgstr \"地震規模\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1110\nmsgid \"震源深度\"\nmsgstr \"震源深度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:886\nmsgid \"沒有更多資料\"\nmsgstr \"沒有更多資料\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1030\nmsgid \"報告頁面\"\nmsgstr \"報告頁面\"\n\n#: ./lib/route/report/report_sheet_content.dart:90\nmsgid \"重播\"\nmsgstr \"重播\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1064\nmsgid \"發震時間\"\nmsgstr \"發震時間\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1132\nmsgid \"各地震度\"\nmsgstr \"各地震度\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1212\nmsgid \"地震報告圖\"\nmsgstr \"地震報告圖\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1228\nmsgid \"震度圖\"\nmsgstr \"震度圖\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1248\nmsgid \"最大地動加速度圖\"\nmsgstr \"最大地動加速度圖\"\n\n#: ./lib/app/map/_lib/managers/report.dart:1268\nmsgid \"最大地動速度圖\"\nmsgstr \"最大地動速度圖\"\n\n#: ./lib/route/announcement/announcement.dart:10\nmsgid \"錯誤\"\nmsgstr \"錯誤\"\n\n#: ./lib/route/announcement/announcement.dart:11\nmsgid \"已解決\"\nmsgstr \"已解決\"\n\n#: ./lib/route/announcement/announcement.dart:12\nmsgid \"影響：小\"\nmsgstr \"影響：小\"\n\n#: ./lib/route/announcement/announcement.dart:13\nmsgid \"影響：中\"\nmsgstr \"影響：中\"\n\n#: ./lib/route/announcement/announcement.dart:14\nmsgid \"影響：大\"\nmsgstr \"影響：大\"\n\n#: ./lib/route/announcement/announcement.dart:16\nmsgid \"維修\"\nmsgstr \"維修\"\n\n#: ./lib/route/announcement/announcement.dart:17\nmsgid \"測試\"\nmsgstr \"測試\"\n\n#: ./lib/route/announcement/announcement.dart:18\nmsgid \"變更\"\nmsgstr \"變更\"\n\n#: ./lib/route/announcement/announcement.dart:19\nmsgid \"完成\"\nmsgstr \"完成\"\n\n#: ./lib/route/announcement/announcement.dart:20\nmsgid \"地震相關\"\nmsgstr \"地震相關\"\n\n#: ./lib/route/announcement/announcement.dart:21\nmsgid \"氣象相關\"\nmsgstr \"氣象相關\"\n\n#: ./lib/route/announcement/announcement.dart:27\nmsgid \"未知\"\nmsgstr \"未知\"\n\n#: ./lib/route/announcement/announcement.dart:105\nmsgid \"目前沒有公告\"\nmsgstr \"目前沒有公告\"\n\n#: ./lib/route/announcement/announcement.dart:246\nmsgid \"公告詳情\"\nmsgstr \"公告詳情\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:73\nmsgid \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\nmsgstr \"請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:123\nmsgid \"已儲存圖片\"\nmsgstr \"已儲存圖片\"\n\n#: ./lib/route/image_viewer/image_viewer.dart:141\nmsgid \"儲存圖片時發生錯誤\"\nmsgstr \"儲存圖片時發生錯誤\"\n\n#: ./lib/utils/extensions/number.dart:26\nmsgid \"０級\"\nmsgstr \"０級\"\n\n#: ./lib/utils/extensions/number.dart:27\nmsgid \"１級\"\nmsgstr \"１級\"\n\n#: ./lib/utils/extensions/number.dart:28\nmsgid \"２級\"\nmsgstr \"２級\"\n\n#: ./lib/utils/extensions/number.dart:29\nmsgid \"３級\"\nmsgstr \"３級\"\n\n#: ./lib/utils/extensions/number.dart:30\nmsgid \"４級\"\nmsgstr \"４級\"\n\n#: ./lib/utils/extensions/number.dart:31\nmsgid \"５弱\"\nmsgstr \"５弱\"\n\n#: ./lib/utils/extensions/number.dart:32\nmsgid \"５強\"\nmsgstr \"５強\"\n\n#: ./lib/utils/extensions/number.dart:33\nmsgid \"６弱\"\nmsgstr \"６弱\"\n\n#: ./lib/utils/extensions/number.dart:34\nmsgid \"６強\"\nmsgstr \"６強\"\n\n#: ./lib/utils/extensions/number.dart:35\nmsgid \"７級\"\nmsgstr \"７級\"\n\n#: ./lib/utils/weather_icon.dart:285\nmsgid \"晴\"\nmsgstr \"晴\"\n\n#: ./lib/utils/weather_icon.dart:286\nmsgid \"晴有霾\"\nmsgstr \"晴有霾\"\n\n#: ./lib/utils/weather_icon.dart:287\nmsgid \"晴有靄\"\nmsgstr \"晴有靄\"\n\n#: ./lib/utils/weather_icon.dart:288\nmsgid \"晴有閃電\"\nmsgstr \"晴有閃電\"\n\n#: ./lib/utils/weather_icon.dart:304\nmsgid \"晴天伴有雷\"\nmsgstr \"晴天伴有雷\"\n\n#: ./lib/utils/weather_icon.dart:290\nmsgid \"晴有霧\"\nmsgstr \"晴有霧\"\n\n#: ./lib/utils/weather_icon.dart:291\nmsgid \"晴有雨\"\nmsgstr \"晴有雨\"\n\n#: ./lib/utils/weather_icon.dart:292\nmsgid \"晴有雨雪\"\nmsgstr \"晴有雨雪\"\n\n#: ./lib/utils/weather_icon.dart:293\nmsgid \"晴有大雪\"\nmsgstr \"晴有大雪\"\n\n#: ./lib/utils/weather_icon.dart:294\nmsgid \"晴有雪珠\"\nmsgstr \"晴有雪珠\"\n\n#: ./lib/utils/weather_icon.dart:295\nmsgid \"晴有冰珠\"\nmsgstr \"晴有冰珠\"\n\n#: ./lib/utils/weather_icon.dart:296\nmsgid \"晴有陣雪\"\nmsgstr \"晴有陣雪\"\n\n#: ./lib/utils/weather_icon.dart:297\nmsgid \"晴陣雨雪\"\nmsgstr \"晴陣雨雪\"\n\n#: ./lib/utils/weather_icon.dart:298\nmsgid \"晴有雹\"\nmsgstr \"晴有雹\"\n\n#: ./lib/utils/weather_icon.dart:299\nmsgid \"晴有雷雨\"\nmsgstr \"晴有雷雨\"\n\n#: ./lib/utils/weather_icon.dart:300\nmsgid \"晴有雷雪\"\nmsgstr \"晴有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:301\nmsgid \"晴有雷雹\"\nmsgstr \"晴有雷雹\"\n\n#: ./lib/utils/weather_icon.dart:302\nmsgid \"晴大雷雨\"\nmsgstr \"晴大雷雨\"\n\n#: ./lib/utils/weather_icon.dart:303\nmsgid \"晴大雷雹\"\nmsgstr \"晴大雷雹\"\n\n#: ./lib/utils/weather_icon.dart:305\nmsgid \"多雲\"\nmsgstr \"多雲\"\n\n#: ./lib/utils/weather_icon.dart:306\nmsgid \"多雲有霾\"\nmsgstr \"多雲有霾\"\n\n#: ./lib/utils/weather_icon.dart:307\nmsgid \"多雲有靄\"\nmsgstr \"多雲有靄\"\n\n#: ./lib/utils/weather_icon.dart:308\nmsgid \"多雲有閃電\"\nmsgstr \"多雲有閃電\"\n\n#: ./lib/utils/weather_icon.dart:324\nmsgid \"多雲伴有雷\"\nmsgstr \"多雲伴有雷\"\n\n#: ./lib/utils/weather_icon.dart:310\nmsgid \"多雲有霧\"\nmsgstr \"多雲有霧\"\n\n#: ./lib/utils/weather_icon.dart:311\nmsgid \"多雲有雨\"\nmsgstr \"多雲有雨\"\n\n#: ./lib/utils/weather_icon.dart:312\nmsgid \"多雲有雨雪\"\nmsgstr \"多雲有雨雪\"\n\n#: ./lib/utils/weather_icon.dart:313\nmsgid \"多雲有大雪\"\nmsgstr \"多雲有大雪\"\n\n#: ./lib/utils/weather_icon.dart:314\nmsgid \"多雲有雪珠\"\nmsgstr \"多雲有雪珠\"\n\n#: ./lib/utils/weather_icon.dart:315\nmsgid \"多雲有冰珠\"\nmsgstr \"多雲有冰珠\"\n\n#: ./lib/utils/weather_icon.dart:316\nmsgid \"多雲有陣雪\"\nmsgstr \"多雲有陣雪\"\n\n#: ./lib/utils/weather_icon.dart:317\nmsgid \"多雲陣雨雪\"\nmsgstr \"多雲陣雨雪\"\n\n#: ./lib/utils/weather_icon.dart:318\nmsgid \"多雲有雹\"\nmsgstr \"多雲有雹\"\n\n#: ./lib/utils/weather_icon.dart:319\nmsgid \"多雲有雷雨\"\nmsgstr \"多雲有雷雨\"\n\n#: ./lib/utils/weather_icon.dart:320\nmsgid \"多雲有雷雪\"\nmsgstr \"多雲有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:321\nmsgid \"多雲有雷雹\"\nmsgstr \"多雲有雷雹\"\n\n#: ./lib/utils/weather_icon.dart:322\nmsgid \"多雲大雷雨\"\nmsgstr \"多雲大雷雨\"\n\n#: ./lib/utils/weather_icon.dart:323\nmsgid \"多雲大雷雹\"\nmsgstr \"多雲大雷雹\"\n\n#: ./lib/utils/weather_icon.dart:325\nmsgid \"陰\"\nmsgstr \"陰\"\n\n#: ./lib/utils/weather_icon.dart:326\nmsgid \"陰有霾\"\nmsgstr \"陰有霾\"\n\n#: ./lib/utils/weather_icon.dart:327\nmsgid \"陰有靄\"\nmsgstr \"陰有靄\"\n\n#: ./lib/utils/weather_icon.dart:328\nmsgid \"陰有閃電\"\nmsgstr \"陰有閃電\"\n\n#: ./lib/utils/weather_icon.dart:344\nmsgid \"陰天伴有雷\"\nmsgstr \"陰天伴有雷\"\n\n#: ./lib/utils/weather_icon.dart:330\nmsgid \"陰有霧\"\nmsgstr \"陰有霧\"\n\n#: ./lib/utils/weather_icon.dart:331\nmsgid \"陰有雨\"\nmsgstr \"陰有雨\"\n\n#: ./lib/utils/weather_icon.dart:332\nmsgid \"陰有雨雪\"\nmsgstr \"陰有雨雪\"\n\n#: ./lib/utils/weather_icon.dart:333\nmsgid \"陰有大雪\"\nmsgstr \"陰有大雪\"\n\n#: ./lib/utils/weather_icon.dart:334\nmsgid \"陰有雪珠\"\nmsgstr \"陰有雪珠\"\n\n#: ./lib/utils/weather_icon.dart:335\nmsgid \"陰有冰珠\"\nmsgstr \"陰有冰珠\"\n\n#: ./lib/utils/weather_icon.dart:336\nmsgid \"陰有陣雪\"\nmsgstr \"陰有陣雪\"\n\n#: ./lib/utils/weather_icon.dart:337\nmsgid \"陰陣雨雪\"\nmsgstr \"陰陣雨雪\"\n\n#: ./lib/utils/weather_icon.dart:338\nmsgid \"陰有雹\"\nmsgstr \"陰有雹\"\n\n#: ./lib/utils/weather_icon.dart:339\nmsgid \"陰有雷雨\"\nmsgstr \"陰有雷雨\"\n\n#: ./lib/utils/weather_icon.dart:340\nmsgid \"陰有雷雪\"\nmsgstr \"陰有雷雪\"\n\n#: ./lib/utils/weather_icon.dart:341\nmsgid \"陰有雷雹\"\nmsgstr \"陰有雷雹\"\n\n#: ./lib/utils/weather_icon.dart:342\nmsgid \"陰大雷雨\"\nmsgstr \"陰大雷雨\"\n\n#: ./lib/utils/weather_icon.dart:343\nmsgid \"陰大雷雹\"\nmsgstr \"陰大雷雹\"\n\n#: ./lib/api/model/location/location.dart:85\nmsgid \"{city}{cityLevel} {town}{townLevel}\"\nmsgstr \"{city}{cityLevel} {town}{townLevel}\"\n\n#: ./lib/api/model/location/location.dart:98\nmsgid \"{city} {town}\"\nmsgstr \"{city} {town}\"\n\n#: ./lib/api/model/location/location.dart:113\nmsgid \"{city}{cityLevel}\"\nmsgstr \"{city}{cityLevel}\"\n\n#: ./lib/api/model/location/location.dart:130\nmsgid \"{town}{townLevel}\"\nmsgstr \"{town}{townLevel}\"\n\n#: ./lib/widgets/ui/color_picker.dart:363\nmsgid \"色相\"\nmsgstr \"色相\"\n\n#: ./lib/widgets/ui/color_picker.dart:379\nmsgid \"彩度\"\nmsgstr \"彩度\"\n\n#: ./lib/widgets/ui/color_picker.dart:395\nmsgid \"明度\"\nmsgstr \"明度\"\n\n#: ./lib/widgets/ui/color_picker.dart:415\nmsgid \"十六進位值\"\nmsgstr \"十六進位值\"\n"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\n  - source: /.crowdin/strings.pot\n    translation: /assets/translations/%osx_locale%.po\n  - source: /.crowdin/*.csv\n    translation: /assets/translations/%original_file_name%\n    multilingual: 1\n    first_line_contains_header: 1\n    scheme: 'identifier,source_phrase,en,ja,ko,ru,vi,zh-CN'\n"
  },
  {
    "path": "devtools_options.yaml",
    "content": "description: This file stores settings for Dart & Flutter DevTools.\ndocumentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states\nextensions:\n  - provider: true\n  - shared_preferences: true"
  },
  {
    "path": "docs/sound.mdx",
    "content": "# 緊急地震速報\n\n## 緊急地震速報(重大)\n\n- 條件：最大震度 5 弱以上以及所在地(鄉鎮)預估震度 4 以上。\n- 音效：eew_alert\n\n## 緊急地震速報(一般)\n\n- 條件：最大震度 5 弱以上以及所在地(鄉鎮)預估震度 2 以上。\n- 音效：eew\n\n## 緊急地震速報(無聲)\n\n- 條件：最大震度 5 弱以上以及所在地(鄉鎮)預估震度 1 以上。\n- 音效：null\n\n## 地震速報(重大)\n\n- 條件：所在地(鄉鎮)預估震度 4 以上。\n- 音效：eew\n\n## 地震速報(一般)\n\n- 條件：所在地(鄉鎮)預估震度 2 以上。\n- 音效：eew\n\n## 地震速報(無聲)\n\n- 條件：所在地(鄉鎮)預估震度 1 以上。\n- 音效：null\n\n# 地震資訊\n\n## 震度速報(一般)\n\n- 條件：所在地(鄉鎮)實測震度 3 以上。\n- 音效：report\n\n## 震度速報(無聲)\n\n- 條件：所在地(鄉鎮)實測震度 1 以上。\n- 音效：null\n\n## 強震監視器(一般)\n\n- 條件：偵測到晃動。\n- 音效：eq\n\n## 地震報告(一般)\n\n- 條件：所在地(縣市)實測震度 3 以上。\n- 音效：report\n\n## 地震報告(無聲)\n\n- 條件：所在地(縣市)實測震度 1 以上。\n- 音效：null\n\n# 雷雨即時訊息\n\n## 重大\n\n- 條件：所在地(鄉鎮)發布山區暴雨時。\n- 音效：rain\n\n## 一般\n\n- 條件：所在地(鄉鎮)發布雷雨即時訊息時。\n- 音效：rain\n\n# 天氣警特報\n\n## 重大\n\n- 條件：所在地(鄉鎮)發布紅色燈號之天氣警特報。\n- 音效：weather\n\n## 一般\n\n- 條件：所在地(鄉鎮)發布上述除外燈號之天氣警特報。\n- 音效：weather\n\n# 防災資訊(防空、土石流、淹水、堰塞湖)\n\n## 重大\n\n- 條件：所在地(鄉鎮)發布防災警訊時。\n- 音效：warn\n\n## 一般\n\n- 條件：所在地(鄉鎮)發布防災資訊時。\n- 音效：normal\n\n# 海嘯資訊\n\n## 重大\n\n- 條件：海嘯警報發布時。\n- 音效：tsunami\n\n## 一般\n\n- 條件：海嘯消息發布時。\n- 音效：normal\n\n## 太平洋海嘯消息(無聲)\n\n- 條件：太平洋海嘯消息發布時。\n- 音效：null\n\n# 其他\n\n## 公告\n\n- 條件：發送公告時。\n- 音效：info\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Profile.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\nplatform :ios, '13.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n\n    target.build_configurations.each do |config|\n      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [\n        '$(inherited)',\n        'PERMISSION_PHOTOS=1',\n        'PERMISSION_LOCATION=1',\n        'PERMISSION_NOTIFICATIONS=1',\n      ]\n      unless target.name == 'Runner'\n      config.build_settings['SKIP_INSTALL'] = \"YES\"\n      end\n    end\n  end\n\n  ################  Awesome Notifications pod modification  ###################\n  awesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks')\n  require awesome_pod_file\n  update_awesome_pod_build_settings(installer)\n  ################  Awesome Notifications pod modification  ###################\nend\n\n################  Awesome Notifications pod modification  ###################\nawesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks')\nrequire awesome_pod_file\nupdate_awesome_main_target_settings('Runner', File.dirname(File.realpath(__FILE__)), flutter_root)\n################  Awesome Notifications pod modification  ###################\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import CoreLocation\nimport Flutter\nimport UIKit\nimport Intents\nimport Photos\nimport UserNotifications\n\n@UIApplicationMain\n@objc\nclass AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {\n    // MARK: - Properties\n\n    private var locationChannel: FlutterMethodChannel?\n    private var locationManager: CLLocationManager!\n    private var lastSentLocation: CLLocation?\n    private var isLocationEnabled: Bool = false\n    private var apnsToken: String?\n    private var backgroundTask: UIBackgroundTaskIdentifier = .invalid\n\n    // MARK: - Application Lifecycle\n\n    override func application(\n        _ application: UIApplication,\n        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n    ) -> Bool {\n        GeneratedPluginRegistrant.register(with: self)\n        setupFlutterChannels()\n        setupLocationManager()\n        \n        if let locationKey = launchOptions?[\n            UIApplication.LaunchOptionsKey.location] as? NSNumber,\n            locationKey.boolValue\n        {\n            startLocationUpdates()\n        } else if isLocationEnabled {\n            startLocationUpdates()\n        }\n\n        return super.application(\n            application, didFinishLaunchingWithOptions: launchOptions)\n    }\n\n    // MARK: - Quick Action\n    override func application(\n        _ application: UIApplication,\n        performActionFor shortcutItem: UIApplicationShortcutItem,\n        completionHandler: @escaping (Bool) -> Void\n    ) {\n        handleShortcut(shortcutItem)\n        completionHandler(true)\n    }\n\n    private func handleShortcut(_ shortcutItem: UIApplicationShortcutItem) {\n        UserDefaults.standard.set(shortcutItem.type, forKey: \"initialShortcut\")\n        notifyFlutterShortcut(shortcutItem.type)\n    }\n    \n    // MARK: - NSUserActivity\n    override func application(\n        _ application: UIApplication,\n        continue userActivity: NSUserActivity,\n        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void\n    ) -> Bool {\n        if userActivity.activityType == \"com.exptech.dpip.monitor\" ||\n           userActivity.activityType == \"OpenMonitorIntentIntent\" {\n            UserDefaults.standard.set(\"monitor\", forKey: \"initialShortcut\")\n            notifyFlutterShortcut(\"monitor\")\n        }\n        return super.application(application, continue: userActivity, restorationHandler: restorationHandler)\n    }\n    \n    private func notifyFlutterShortcut(_ value: String) {\n        guard let controller =\n            window?.rootViewController as? FlutterViewController\n        else { return }\n\n        let channel = FlutterMethodChannel(\n            name: \"com.exptech.dpip/shortcut\",\n            binaryMessenger: controller.binaryMessenger\n        )\n        channel.invokeMethod(\"onShortcut\", arguments: value)\n    }\n\n    // MARK: - Background Handling\n    override func applicationDidEnterBackground(_ application: UIApplication) {\n        startBackgroundTask()\n    }\n    \n    override func applicationDidBecomeActive(_ application: UIApplication) {\n        super.applicationDidBecomeActive(application)\n    }\n\n    // MARK: - Setup Methods\n\n    private func setupFlutterChannels() {\n        guard let controller = window?.rootViewController as? FlutterViewController else { return }\n\n        locationChannel = FlutterMethodChannel(\n            name: \"com.exptech.dpip/location\",\n            binaryMessenger: controller.binaryMessenger)\n\n        locationChannel?.setMethodCallHandler { [weak self] (call, result) in\n            self?.handleLocationChannelCall(call, result: result)\n        }\n\n        let shortcutChannel = FlutterMethodChannel(\n            name: \"com.exptech.dpip/shortcut\",\n            binaryMessenger: controller.binaryMessenger\n        )\n\n        shortcutChannel.setMethodCallHandler { call, result in\n            if call.method == \"getInitialShortcut\" {\n                let shortcut = UserDefaults.standard.string(forKey: \"initialShortcut\")\n                result(shortcut)\n                UserDefaults.standard.removeObject(forKey: \"initialShortcut\")\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        }\n        \n        let imageSaverChannel = FlutterMethodChannel(\n            name: \"image_saver\",\n            binaryMessenger: controller.binaryMessenger\n        )\n\n        imageSaverChannel.setMethodCallHandler { call, result in\n            if call.method == \"saveImage\",\n               let args = call.arguments as? [String: Any],\n               let path = args[\"path\"] as? String {\n\n                if let image = UIImage(contentsOfFile: path) {\n                    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)\n                    result(nil)\n                } else {\n                    result(FlutterError(\n                        code: \"INVALID_IMAGE\",\n                        message: \"Cannot load image from path\",\n                        details: nil\n                    ))\n                }\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        }\n    }\n    \n    private func setupLocationManager() {\n        locationManager = CLLocationManager()\n        locationManager.delegate = self\n        locationManager.allowsBackgroundLocationUpdates = true\n        locationManager.pausesLocationUpdatesAutomatically = false\n        isLocationEnabled = UserDefaults.standard.bool(\n            forKey: \"locationSendingEnabled\")\n    }\n    \n    // MARK: - APNS Token Handling\n    \n    override func application(\n        _ application: UIApplication,\n        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data\n    ) {\n        let tokenString = deviceToken.map { String(format: \"%02.2hhx\", $0) }.joined()\n        apnsToken = tokenString\n        print(\"APNS token: \\(tokenString)\")\n    }\n    \n    override func application(\n        _ application: UIApplication,\n        didFailToRegisterForRemoteNotificationsWithError error: Error\n    ) {\n        print(\"Failed to register for remote notifications: \\(error)\")\n    }\n    \n    // MARK: - Channel Handlers\n    \n    private func handleLocationChannelCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n        guard call.method == \"toggleLocation\",\n              let args = call.arguments as? [String: Any],\n              let isEnabled = args[\"isEnabled\"] as? Bool else {\n            result(FlutterError(code: \"INVALID_ARGUMENT\", message: \"Invalid argument\", details: nil))\n            return\n        }\n\n        toggleLocation(isEnabled: isEnabled)\n        result(\"Location toggled\")\n    }\n    \n    // MARK: - Location Management\n    \n    private func toggleLocation(isEnabled: Bool) {\n        isLocationEnabled = isEnabled\n        UserDefaults.standard.set(isEnabled, forKey: \"locationSendingEnabled\")\n        \n        if isEnabled {\n            startLocationUpdates()\n            if lastSentLocation == nil, let currentLocation = locationManager.location {\n                sendLocationToServer(location: currentLocation)\n            }\n        } else {\n            stopLocationUpdates()\n        }\n    }\n    \n    private func startLocationUpdates() {\n        guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {\n            print(\"Significant location change monitoring is not available\")\n            return\n        }\n        \n        locationManager.startMonitoringSignificantLocationChanges()\n        \n        if let lastLocation = locationManager.location {\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in\n                self?.updateRegionMonitoring(for: lastLocation)\n            }\n        }\n    }\n    \n    private func stopLocationUpdates() {\n        locationManager.stopMonitoringSignificantLocationChanges()\n        locationManager.stopUpdatingLocation()\n        locationManager.monitoredRegions.forEach { locationManager.stopMonitoring(for: $0) }\n    }\n    \n    private func updateRegionMonitoring(for location: CLLocation) {\n        locationManager.monitoredRegions.forEach { locationManager.stopMonitoring(for: $0) }\n        \n        let region = CLCircularRegion(\n            center: location.coordinate,\n            radius: 250,\n            identifier: \"currentRegion\"\n        )\n        region.notifyOnEntry = false\n        region.notifyOnExit = true\n        \n        locationManager.startMonitoring(for: region)\n    }\n    \n    // MARK: - Location Delegate Methods\n    \n    func locationManager(\n        _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]\n    ) {\n        guard isLocationEnabled, let location = locations.last else { return }\n        sendLocationToServer(location: location)\n        lastSentLocation = location\n    }\n\n    func locationManager(\n        _ manager: CLLocationManager, didExitRegion region: CLRegion\n    ) {\n        if let location = manager.location {\n            sendLocationToServer(location: location)\n        }\n    }\n    \n    // MARK: - Network\n    \n    private func sendLocationToServer(location: CLLocation) {\n        guard isLocationEnabled else { return }\n        guard let token = apnsToken else { return }\n        \n        let latitude = location.coordinate.latitude\n        let longitude = location.coordinate.longitude\n        let appVersion =\n            Bundle.main.object(\n                forInfoDictionaryKey: \"CFBundleShortVersionString\") as? String\n            ?? \"Unknown\"\n\n        let urlString =\n            \"https://api-1.exptech.dev/api/v2/location/1/\\(token)/\\(appVersion)/\\(latitude),\\(longitude)\"\n        guard let url = URL(string: urlString) else { return }\n\n        var request = URLRequest(url: url)\n        request.httpMethod = \"GET\"\n\n        let task = URLSession.shared.dataTask(with: request) {\n            [weak self] data, response, error in\n            guard let self = self, self.isLocationEnabled else { return }\n\n            if let error = error {\n                print(\"Error sending location: \\(error)\")\n                return\n            }\n\n            DispatchQueue.main.async {\n                self.updateRegionMonitoring(for: location)\n            }\n        }\n\n        task.resume()\n    }\n    \n    // MARK: - Background Task Management\n    \n    private func startBackgroundTask() {\n        backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in\n            self?.endBackgroundTask()\n        }\n        \n        DispatchQueue.global().async { [weak self] in\n            self?.performExtendedBackgroundTasks()\n            self?.endBackgroundTask()\n        }\n    }\n    \n    private func performExtendedBackgroundTasks() {\n        if let location = locationManager.location {\n            sendLocationToServer(location: location)\n        }\n    }\n    \n    private func endBackgroundTask() {\n        if backgroundTask != .invalid {\n            UIApplication.shared.endBackgroundTask(backgroundTask)\n            backgroundTask = .invalid\n        }\n    }\n    \n    // MARK: - URL Session Handling\n    \n    override func application(\n        _ application: UIApplication,\n        handleEventsForBackgroundURLSession identifier: String,\n        completionHandler: @escaping () -> Void\n    ) {\n        completionHandler()\n    }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"size\": \"20x20\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-20x20@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"20x20\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-20x20@3x.png\",\n      \"scale\": \"3x\"\n    },\n    {\n      \"size\": \"29x29\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-29x29@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"29x29\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-29x29@3x.png\",\n      \"scale\": \"3x\"\n    },\n    {\n      \"size\": \"40x40\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-40x40@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"40x40\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-40x40@3x.png\",\n      \"scale\": \"3x\"\n    },\n    {\n      \"size\": \"60x60\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-60x60@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"60x60\",\n      \"idiom\": \"iphone\",\n      \"filename\": \"Icon-App-60x60@3x.png\",\n      \"scale\": \"3x\"\n    },\n    {\n      \"size\": \"20x20\",\n      \"idiom\": \"ipad\",\n      \"filename\": \"Icon-App-20x20@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"29x29\",\n      \"idiom\": \"ipad\",\n      \"filename\": \"Icon-App-29x29@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"40x40\",\n      \"idiom\": \"ipad\",\n      \"filename\": \"Icon-App-40x40@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"76x76\",\n      \"idiom\": \"ipad\",\n      \"filename\": \"Icon-App-76x76@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"83.5x83.5\",\n      \"idiom\": \"ipad\",\n      \"filename\": \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"1024x1024\",\n      \"idiom\": \"ios-marketing\",\n      \"filename\": \"Icon-App-1024x1024.heic\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"size\": \"32x32\",\n      \"idiom\": \"mac\",\n      \"filename\": \"32.png\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"size\": \"32x32\",\n      \"idiom\": \"mac\",\n      \"filename\": \"64.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"128x128\",\n      \"idiom\": \"mac\",\n      \"filename\": \"128.png\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"size\": \"128x128\",\n      \"idiom\": \"mac\",\n      \"filename\": \"256.heic\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"256x256\",\n      \"idiom\": \"mac\",\n      \"filename\": \"256.heic\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"size\": \"256x256\",\n      \"idiom\": \"mac\",\n      \"filename\": \"512.heic\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"size\": \"512x512\",\n      \"idiom\": \"mac\",\n      \"filename\": \"512.heic\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"size\": \"512x512\",\n      \"idiom\": \"mac\",\n      \"filename\": \"Icon-App-1024x1024.heic\",\n      \"scale\": \"2x\"\n    }\n  ],\n  \"info\": {\n    \"author\": \"xcode\",\n    \"version\": 1\n  }\n}"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/Base.lproj/InfoPlist.strings",
    "content": "\"CFBundleDisplayName\" = \"DPIP\";\n"
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"23504\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <device id=\"retina6_12\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"23506\"/>\n        <capability name=\"System colors in document resources\" minToolsVersion=\"11.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"393\" height=\"852\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleAspectFit\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                                <rect key=\"frame\" x=\"137.66666666666666\" y=\"367\" width=\"117.66666666666666\" height=\"118\"/>\n                                <constraints>\n                                    <constraint firstAttribute=\"height\" secondItem=\"YRO-k0-Ey4\" secondAttribute=\"width\" id=\"aspect-ratio-1to1\"/>\n                                </constraints>\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"center-x\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"center-y\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"width\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"width\" multiplier=\"0.3\" id=\"width-30-percent\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"80.916030534351137\" y=\"264.08450704225356\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"341.33334350585938\" height=\"341.33334350585938\"/>\n        <systemColor name=\"systemBackgroundColor\">\n            <color white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/GoogleService-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>API_KEY</key>\n\t<string>AIzaSyALF1O3Celks5H6i3iY43sdMlC4FW9deHw</string>\n\t<key>GCM_SENDER_ID</key>\n\t<string>141632948166</string>\n\t<key>PLIST_VERSION</key>\n\t<string>1</string>\n\t<key>BUNDLE_ID</key>\n\t<string>com.exptech.dpip.dpip</string>\n\t<key>PROJECT_ID</key>\n\t<string>dpip-a658c</string>\n\t<key>STORAGE_BUCKET</key>\n\t<string>dpip-a658c.appspot.com</string>\n\t<key>IS_ADS_ENABLED</key>\n\t<false></false>\n\t<key>IS_ANALYTICS_ENABLED</key>\n\t<false></false>\n\t<key>IS_APPINVITE_ENABLED</key>\n\t<true></true>\n\t<key>IS_GCM_ENABLED</key>\n\t<true></true>\n\t<key>IS_SIGNIN_ENABLED</key>\n\t<true></true>\n\t<key>GOOGLE_APP_ID</key>\n\t<string>1:141632948166:ios:15ef51ceb6c19e5b9e14c7</string>\n</dict>\n</plist>"
  },
  {
    "path": "ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>BGTaskSchedulerPermittedIdentifiers</key>\n\t<array>\n\t\t<string>dev.flutter.background.refresh</string>\n\t</array>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>DPIP</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleLocalizations</key>\n\t<array>\n\t\t<string>zh_TW</string>\n\t\t<string>zh</string>\n\t\t<string>ja</string>\n\t\t<string>en</string>\n\t\t<string>ko</string>\n\t\t<string>vi</string>\n\t</array>\n\t<key>CFBundleName</key>\n\t<string>dpip</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>ITSAppUsesNonExemptEncryption</key>\n\t<false/>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSBonjourServices</key>\n\t<array>\n\t\t<string>${DART_DEBUG_BONJOUR_SERVICE}</string>\n\t</array>\n\t<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>\n\t<string>DPIP 將利用你的位置資訊，用以自動設定所在地並在地震發生時能較準確地預估所在地的最大震度。</string>\n\t<key>NSLocationAlwaysUsageDescription</key>\n\t<string>DPIP 在開啟和後台執行時需要位置資訊，用以自動設定所在地並在地震發生時能較準確地預估所在地的最大震度。</string>\n\t<key>NSLocationWhenInUseUsageDescription</key>\n\t<string>DPIP 將利用你的位置資訊，用以自動設定所在地並在地震發生時能較準確地預估所在地的最大震度。</string>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string>用於儲存地震報告圖片。</string>\n\t<key>NSUserActivityTypes</key>\n\t<array>\n\t\t<string>OpenMonitorIntentIntent</string>\n\t\t<string>com.exptech.dpip.monitor</string>\n\t</array>\n\t<key>UIApplicationShortcutItems</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>UIApplicationShortcutItemIconType</key>\n\t\t\t<string>UIApplicationShortcutIconTypeFavorite</string>\n\t\t\t<key>UIApplicationShortcutItemTitle</key>\n\t\t\t<string>強震監視器</string>\n\t\t\t<key>UIApplicationShortcutItemType</key>\n\t\t\t<string>monitor</string>\n\t\t</dict>\n\t</array>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n\t<key>UIBackgroundModes</key>\n\t<array>\n\t\t<string>fetch</string>\n\t\t<string>location</string>\n\t\t<string>remote-notification</string>\n\t</array>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner/Runner.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.usernotifications.critical-alerts</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/RunnerProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.usernotifications.critical-alerts</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/en.lproj/Monitor.strings",
    "content": "\"l2uH53\" = \"This shortcut opens the Earthquake Monitor screen directly\";\n\n\"qno9IY\" = \"Open Earthquake Monitor\";\n\n"
  },
  {
    "path": "ios/Runner/ja.lproj/InfoPlist.strings",
    "content": "\"CFBundleDisplayName\" = \"DPIP防災\";\n"
  },
  {
    "path": "ios/Runner/ja.lproj/Monitor.strings",
    "content": "\"l2uH53\" = \"このショートカットは強震モニター画面を直接開きます\";\n\n\"qno9IY\" = \"強震モニターを開く\";\n\n"
  },
  {
    "path": "ios/Runner/ko.lproj/InfoPlist.strings",
    "content": "\"CFBundleDisplayName\" = \"DPIP 재해\";\n"
  },
  {
    "path": "ios/Runner/ko.lproj/Monitor.strings",
    "content": "\"l2uH53\" = \"이 단축키는 강진 모니터 화면을 직접 엽니다\";\n\n\"qno9IY\" = \"강진 모니터 열기\";\n\n"
  },
  {
    "path": "ios/Runner/zh.lproj/InfoPlist.strings",
    "content": "\"CFBundleDisplayName\" = \"DPIP 防灾\";\n"
  },
  {
    "path": "ios/Runner/zh_TW.lproj/InfoPlist.strings",
    "content": "\"CFBundleDisplayName\" = \"DPIP 防災\";\n"
  },
  {
    "path": "ios/Runner/zh_TW.lproj/Monitor.intentdefinition",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>INEnums</key>\n\t<array/>\n\t<key>INIntentDefinitionModelVersion</key>\n\t<string>1.2</string>\n\t<key>INIntentDefinitionNamespace</key>\n\t<string>tuhqJ3</string>\n\t<key>INIntentDefinitionSystemVersion</key>\n\t<string>25B78</string>\n\t<key>INIntentDefinitionToolsBuildVersion</key>\n\t<string>17B100</string>\n\t<key>INIntentDefinitionToolsVersion</key>\n\t<string>26.1.1</string>\n\t<key>INIntents</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>INIntentCategory</key>\n\t\t\t<string>information</string>\n\t\t\t<key>INIntentConfigurable</key>\n\t\t\t<true/>\n\t\t\t<key>INIntentDescription</key>\n\t\t\t<string>此捷徑直接開啟強震監視器畫面</string>\n\t\t\t<key>INIntentDescriptionID</key>\n\t\t\t<string>l2uH53</string>\n\t\t\t<key>INIntentManagedParameterCombinations</key>\n\t\t\t<dict>\n\t\t\t\t<key></key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>INIntentParameterCombinationSupportsBackgroundExecution</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>INIntentParameterCombinationUpdatesLinked</key>\n\t\t\t\t\t<true/>\n\t\t\t\t</dict>\n\t\t\t</dict>\n\t\t\t<key>INIntentName</key>\n\t\t\t<string>OpenMonitorIntent</string>\n\t\t\t<key>INIntentParameterCombinations</key>\n\t\t\t<dict>\n\t\t\t\t<key></key>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>INIntentParameterCombinationIsPrimary</key>\n\t\t\t\t\t<true/>\n\t\t\t\t\t<key>INIntentParameterCombinationSupportsBackgroundExecution</key>\n\t\t\t\t\t<true/>\n\t\t\t\t</dict>\n\t\t\t</dict>\n\t\t\t<key>INIntentResponse</key>\n\t\t\t<dict>\n\t\t\t\t<key>INIntentResponseCodes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>INIntentResponseCodeName</key>\n\t\t\t\t\t\t<string>success</string>\n\t\t\t\t\t\t<key>INIntentResponseCodeSuccess</key>\n\t\t\t\t\t\t<true/>\n\t\t\t\t\t</dict>\n\t\t\t\t\t<dict>\n\t\t\t\t\t\t<key>INIntentResponseCodeName</key>\n\t\t\t\t\t\t<string>failure</string>\n\t\t\t\t\t</dict>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t\t<key>INIntentTitle</key>\n\t\t\t<string>強震監視器</string>\n\t\t\t<key>INIntentTitleID</key>\n\t\t\t<string>qno9IY</string>\n\t\t\t<key>INIntentType</key>\n\t\t\t<string>Custom</string>\n\t\t\t<key>INIntentVerb</key>\n\t\t\t<string>View</string>\n\t\t</dict>\n\t</array>\n\t<key>INTypes</key>\n\t<array/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t528548E22F06357A0027C627 /* Monitor.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 528548E12F06357A0027C627 /* Monitor.intentdefinition */; };\n\t\t529C27C82C93F7B200AAFAB6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 529C27C62C93F7B200AAFAB6 /* InfoPlist.strings */; };\n\t\t52FA5E152DC8A9EA0008FEB0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */; };\n\t\t6037C7C000DE0752E32B9E54 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6838AADB908B55100C5691B /* Pods_Runner.framework */; };\n\t\t632125292C2EA17900A088F8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 632125282C2EA17900A088F8 /* GoogleService-Info.plist */; };\n\t\t63F1FCBF2C8D48D300693F0C /* rain.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB82C8D48D300693F0C /* rain.aiff */; };\n\t\t63F1FCC02C8D48D300693F0C /* eew.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB22C8D48D300693F0C /* eew.aiff */; };\n\t\t63F1FCC22C8D48D300693F0C /* info.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB52C8D48D300693F0C /* info.aiff */; };\n\t\t63F1FCC32C8D48D300693F0C /* normal.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB72C8D48D300693F0C /* normal.aiff */; };\n\t\t63F1FCC42C8D48D300693F0C /* warn.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCBC2C8D48D300693F0C /* warn.aiff */; };\n\t\t63F1FCC52C8D48D300693F0C /* weather.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCBD2C8D48D300693F0C /* weather.aiff */; };\n\t\t63F1FCC62C8D48D300693F0C /* report.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB92C8D48D300693F0C /* report.aiff */; };\n\t\t63F1FCC72C8D48D300693F0C /* tsunami.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCBA2C8D48D300693F0C /* tsunami.aiff */; };\n\t\t63F1FCC82C8D48D300693F0C /* eew_alert.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB32C8D48D300693F0C /* eew_alert.aiff */; };\n\t\t63F1FCC92C8D48D300693F0C /* eq.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 63F1FCB42C8D48D300693F0C /* eq.aiff */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n\t\tB3C0548ABD2F128E6EE128FD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t17F64C367FDB32D5C770A605 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.debug.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t5228AD5A2C2EE45D007635F5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = \"<group>\"; };\n\t\t523A5FD82EB21EC0006F93FC /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = \"<group>\"; };\n\t\t526767E42F064A19003CE2E4 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = zh_TW; path = zh_TW.lproj/Monitor.intentdefinition; sourceTree = \"<group>\"; };\n\t\t526767E62F064A3B003CE2E4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Monitor.strings; sourceTree = \"<group>\"; };\n\t\t526767E82F064A3E003CE2E4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Monitor.strings; sourceTree = \"<group>\"; };\n\t\t526767EA2F064A47003CE2E4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Monitor.strings; sourceTree = \"<group>\"; };\n\t\t528548A52F06047F0027C627 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };\n\t\t529C27C92C93F7B900AAFAB6 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t529C27CC2C93F7BC00AAFAB6 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t529C27CF2C947EFB00AAFAB6 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t529C27DC2C97019E00AAFAB6 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = zh_TW.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };\n\t\t632125282C2EA17900A088F8 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = \"GoogleService-Info.plist\"; sourceTree = \"<group>\"; };\n\t\t6321252A2C2EA20700A088F8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t63F1FCB22C8D48D300693F0C /* eew.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eew.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB32C8D48D300693F0C /* eew_alert.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eew_alert.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB42C8D48D300693F0C /* eq.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = eq.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB52C8D48D300693F0C /* info.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = info.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB72C8D48D300693F0C /* normal.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = normal.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB82C8D48D300693F0C /* rain.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = rain.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCB92C8D48D300693F0C /* report.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = report.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCBA2C8D48D300693F0C /* tsunami.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = tsunami.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCBC2C8D48D300693F0C /* warn.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = warn.aiff; sourceTree = \"<group>\"; };\n\t\t63F1FCBD2C8D48D300693F0C /* weather.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = weather.aiff; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t7F429ED347E85746A9B0DD8B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.release.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t9A85FE1E2D76EB310CAE72FD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tC6838AADB908B55100C5691B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tDF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.profile.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t0166E768B450CE02B1DB74B0 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tB3C0548ABD2F128E6EE128FD /* Pods_RunnerTests.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t52FA5E152DC8A9EA0008FEB0 /* StoreKit.framework in Frameworks */,\n\t\t\t\t6037C7C000DE0752E32B9E54 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t5437AE1A0D322629A5F820A0 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t7F429ED347E85746A9B0DD8B /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t9A85FE1E2D76EB310CAE72FD /* Pods-Runner.release.xcconfig */,\n\t\t\t\t17F64C367FDB32D5C770A605 /* Pods-Runner.profile.xcconfig */,\n\t\t\t\t4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */,\n\t\t\t\t86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */,\n\t\t\t\tDF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t523A5FD82EB21EC0006F93FC /* Profile.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\n\t\t\t\t5437AE1A0D322629A5F820A0 /* Pods */,\n\t\t\t\tFC6E272583C3F463DAF5FB4E /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t5228AD5A2C2EE45D007635F5 /* Runner.entitlements */,\n\t\t\t\t63F1FCB22C8D48D300693F0C /* eew.aiff */,\n\t\t\t\t63F1FCB32C8D48D300693F0C /* eew_alert.aiff */,\n\t\t\t\t63F1FCB42C8D48D300693F0C /* eq.aiff */,\n\t\t\t\t63F1FCB52C8D48D300693F0C /* info.aiff */,\n\t\t\t\t63F1FCB72C8D48D300693F0C /* normal.aiff */,\n\t\t\t\t63F1FCB82C8D48D300693F0C /* rain.aiff */,\n\t\t\t\t63F1FCB92C8D48D300693F0C /* report.aiff */,\n\t\t\t\t63F1FCBA2C8D48D300693F0C /* tsunami.aiff */,\n\t\t\t\t63F1FCBC2C8D48D300693F0C /* warn.aiff */,\n\t\t\t\t63F1FCBD2C8D48D300693F0C /* weather.aiff */,\n\t\t\t\t6321252A2C2EA20700A088F8 /* RunnerProfile.entitlements */,\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t632125282C2EA17900A088F8 /* GoogleService-Info.plist */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t\t529C27C62C93F7B200AAFAB6 /* InfoPlist.strings */,\n\t\t\t\t528548E12F06357A0027C627 /* Monitor.intentdefinition */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tFC6E272583C3F463DAF5FB4E /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t52FA5E142DC8A9EA0008FEB0 /* StoreKit.framework */,\n\t\t\t\tC6838AADB908B55100C5691B /* Pods_Runner.framework */,\n\t\t\t\t8A29CCFC3290C53FDF724288 /* Pods_RunnerTests.framework */,\n\t\t\t\t528548A52F06047F0027C627 /* Intents.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t745AFA5621F4079D8F5F66F5 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t\t0166E768B450CE02B1DB74B0 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6BBD6C8E8A651C2033EB74CF /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t127FFB7A93668EDC7C84088F /* [CP] Embed Pods Frameworks */,\n\t\t\t\t81A7F11822CA9F8CECB2FD48 /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 2610;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 13.0\";\n\t\t\tdevelopmentRegion = zh_TW;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\tBase,\n\t\t\t\tzh_TW,\n\t\t\t\ten,\n\t\t\t\tja,\n\t\t\t\tko,\n\t\t\t\tzh,\n\t\t\t\tvi,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t63F1FCBF2C8D48D300693F0C /* rain.aiff in Resources */,\n\t\t\t\t63F1FCC02C8D48D300693F0C /* eew.aiff in Resources */,\n\t\t\t\t63F1FCC22C8D48D300693F0C /* info.aiff in Resources */,\n\t\t\t\t529C27C82C93F7B200AAFAB6 /* InfoPlist.strings in Resources */,\n\t\t\t\t63F1FCC32C8D48D300693F0C /* normal.aiff in Resources */,\n\t\t\t\t63F1FCC42C8D48D300693F0C /* warn.aiff in Resources */,\n\t\t\t\t63F1FCC52C8D48D300693F0C /* weather.aiff in Resources */,\n\t\t\t\t63F1FCC62C8D48D300693F0C /* report.aiff in Resources */,\n\t\t\t\t63F1FCC72C8D48D300693F0C /* tsunami.aiff in Resources */,\n\t\t\t\t63F1FCC82C8D48D300693F0C /* eew_alert.aiff in Resources */,\n\t\t\t\t63F1FCC92C8D48D300693F0C /* eq.aiff in Resources */,\n\t\t\t\t632125292C2EA17900A088F8 /* GoogleService-Info.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t127FFB7A93668EDC7C84088F /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t6BBD6C8E8A651C2033EB74CF /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t745AFA5621F4079D8F5F66F5 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t81A7F11822CA9F8CECB2FD48 /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t\t528548E22F06357A0027C627 /* Monitor.intentdefinition in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t528548E12F06357A0027C627 /* Monitor.intentdefinition */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t526767E42F064A19003CE2E4 /* zh_TW */,\n\t\t\t\t526767E62F064A3B003CE2E4 /* en */,\n\t\t\t\t526767E82F064A3E003CE2E4 /* ja */,\n\t\t\t\t526767EA2F064A47003CE2E4 /* ko */,\n\t\t\t);\n\t\t\tname = Monitor.intentdefinition;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t529C27C62C93F7B200AAFAB6 /* InfoPlist.strings */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t529C27C92C93F7B900AAFAB6 /* zh */,\n\t\t\t\t529C27CC2C93F7BC00AAFAB6 /* ko */,\n\t\t\t\t529C27CF2C947EFB00AAFAB6 /* ja */,\n\t\t\t\t529C27DC2C97019E00AAFAB6 /* zh_TW */,\n\t\t\t);\n\t\t\tname = InfoPlist.strings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = NO;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = NO;\n\t\t\t\tCLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphoneos*]\" = \"armv7 armv7s i386\";\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphonesimulator*]\" = \"x86_64 i386\";\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = DPIP;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 4B4531DD011F7A688ACAA691 /* Pods-RunnerTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 86D7674C61D44CF5E96872D5 /* Pods-RunnerTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = DF6CF45C0AFEE964FC43297A /* Pods-RunnerTests.profile.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_SYMBOLS_PRIVATE_EXTERN = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLLVM_LTO = YES_THIN;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = NO;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = NO;\n\t\t\t\tCLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphoneos*]\" = \"armv7 armv7s i386\";\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphonesimulator*]\" = \"x86_64 i386\";\n\t\t\t\tGCC_SYMBOLS_PRIVATE_EXTERN = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = DPIP;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tAPPLICATION_EXTENSION_API_ONLY = NO;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_OPTIMIZATION = space;\n\t\t\t\tBUILD_LIBRARY_FOR_DISTRIBUTION = NO;\n\t\t\t\tCLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOPY_PHASE_STRIP = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEPLOYMENT_POSTPROCESSING = YES;\n\t\t\t\tDEVELOPMENT_TEAM = 98Q7JARYZF;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphoneos*]\" = \"armv7 armv7s i386\";\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphonesimulator*]\" = \"x86_64 i386\";\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = DPIP;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "lib/api/_client.dart",
    "content": "part of 'exptech.dart';\n\n/// Thrown when RTS data is unavailable for the requested time.\nclass Rtsnodata implements Exception {\n  /// Creates a [Rtsnodata] exception.\n  const Rtsnodata();\n}\n\n/// Wraps [IOHttpClientAdapter] to add zstd decompression support and proxy config.\nclass _ZstdAdapter implements HttpClientAdapter {\n  final Zstandard _zstd = Zstandard();\n  final IOHttpClientAdapter _inner;\n\n  _ZstdAdapter(this._inner);\n\n  @override\n  Future<ResponseBody> fetch(\n    RequestOptions options,\n    Stream<Uint8List>? requestStream,\n    Future<void>? cancelFuture,\n  ) async {\n    options.headers['Accept-Encoding'] = 'gzip, deflate, zstd';\n    final response = await _inner.fetch(options, requestStream, cancelFuture);\n\n    final contentEncoding = response.headers['content-encoding']?.firstOrNull?.toLowerCase();\n    if (contentEncoding == 'zstd') {\n      final builder = BytesBuilder(copy: false);\n      await for (final chunk in response.stream) {\n        builder.add(chunk);\n      }\n      final decompressed = await _zstd.decompress(builder.takeBytes());\n      if (decompressed != null) {\n        final headers = Map<String, List<String>>.from(response.headers)\n          ..remove('content-encoding');\n        return ResponseBody.fromBytes(\n          decompressed,\n          response.statusCode,\n          headers: headers,\n          statusMessage: response.statusMessage,\n          isRedirect: response.isRedirect,\n        );\n      }\n    }\n\n    return response;\n  }\n\n  @override\n  void close({bool force = false}) => _inner.close(force: force);\n}\n\nIOHttpClientAdapter _createInnerAdapter() => IOHttpClientAdapter(\n  createHttpClient: () {\n    final client = HttpClient();\n    final proxyEnabled = Preference.proxyEnabled ?? false;\n    final proxyHost = Preference.proxyHost;\n    final proxyPort = Preference.proxyPort;\n    if (proxyEnabled && proxyHost != null && proxyPort != null) {\n      client.findProxy = (_) => 'PROXY $proxyHost:$proxyPort';\n      client.badCertificateCallback = (_, __, ___) => false;\n    }\n    return client;\n  },\n);\n\nDio _createDio() => Dio()..httpClientAdapter = _ZstdAdapter(_createInnerAdapter());\n\nfinal _cacheOptions = CacheOptions(\n  store: MemCacheStore(),\n  policy: .request,\n  hitCacheOnErrorExcept: [401, 403],\n);\n\nfinal Dio _dio = _createDio();\nfinal Dio _cachedDio = _createDio()..interceptors.add(DioCacheInterceptor(options: _cacheOptions));\n"
  },
  {
    "path": "lib/api/endpoints/app.dart",
    "content": "part of '../exptech.dart';\n\n/// App meta endpoint methods.\nmixin AppEndpoints {\n  /// Fetches Crowdin localization progress for all languages.\n  Future<Result<List<CrowdinLocalizationProgress>, String>> getLocalizationProgress() async {\n    try {\n      final res = await _dio.get('https://exptech.dev/api/v1/dpip/locale');\n      final data = (res.data['data'] as List).cast<Map<String, dynamic>>();\n      return Ok(data.map(CrowdinLocalizationProgress.fromMap).toList());\n    } catch (_) {\n      return const Err('無法從 Crowdin 取得翻譯狀態');\n    }\n  }\n\n  /// Fetches GitHub release notes for DPIP Pocket.\n  Future<Result<List<GithubRelease>, String>> getReleases() async {\n    try {\n      final res = await _dio.get(\n        'https://api.github.com/repos/ExpTechTW/DPIP-Pocket/releases',\n      );\n      return Ok(\n        (res.data as List).cast<Map<String, dynamic>>().map(GithubRelease.fromMap).toList(),\n      );\n    } catch (_) {\n      return const Err('無法從 GitHub 取得更新紀錄');\n    }\n  }\n\n  /// Fetches the current announcements.\n  Future<List<Announcement>> getAnnouncement() async {\n    final res = await _dio.get('${api(1)}/v1/dpip/announcement');\n    return (res.data as List).map((e) => Announcement.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches the notification history.\n  Future<List<NotificationRecord>> getNotificationHistory() async {\n    final res = await _dio.get('${api(1)}/v1/notify/history');\n    return (res.data as List)\n        .map((e) => NotificationRecord.fromMap(e as Map<String, dynamic>))\n        .toList();\n  }\n\n  /// Fetches server status for the past 24 hours.\n  Future<List<ServerStatus>> getStatus() async {\n    final res = await _dio.get(\n      'https://status.exptech.dev/api/v1/status/data',\n      queryParameters: {'duration': '1d'},\n    );\n    return (res.data as List).map((e) => ServerStatus.fromMap(e as Map<String, dynamic>)).toList();\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/device.dart",
    "content": "part of '../exptech.dart';\n\n/// Device, NTP, and notification endpoint methods.\nmixin DeviceEndpoints {\n  /// Returns the current server time in milliseconds since epoch.\n  Future<int> getNtp() async {\n    final t1 = DateTime.now().microsecondsSinceEpoch;\n    final res = await _dio.get(\n      '${ntpBase}/ntp',\n      options: Options(responseType: .plain),\n    );\n    final t4 = DateTime.now().microsecondsSinceEpoch;\n\n    final t2Header = res.headers.value('x-ntp-t2');\n    final t3Header = res.headers.value('x-ntp-t3');\n    final t2 = t2Header != null ? (double.parse(t2Header) * 1000).toInt() : null;\n    final t3 = t3Header != null ? (double.parse(t3Header) * 1000).toInt() : null;\n\n    if (t2 != null && t3 != null) {\n      final offset = ((t2 - t1) + (t3 - t4)) / 2;\n      return (t3 + offset).toInt() ~/ 1000;\n    }\n\n    return double.parse(res.data as String).toInt();\n  }\n\n  /// 回傳所在地\n  Future<String> updateDeviceLocation({\n    required String token,\n    required LatLng coordinates,\n  }) async {\n    if (token.isEmpty) throw ArgumentError.value(token, 'token', 'Token is empty');\n\n    final platform = Platform.isIOS ? 1 : 0;\n    final version = Global.packageInfo.version;\n    final res = await _dio.get(\n      '${api(1)}/v2/location/$platform/$token/$version/${coordinates.latitude},${coordinates.longitude}',\n    );\n\n    if (res.statusCode == 200) {\n      Preference.lastUpdateToServerTime = DateTime.now().millisecondsSinceEpoch;\n      return res.data.toString();\n    }\n\n    return '${res.statusCode} ${res.requestOptions.uri}';\n  }\n\n  /// 取得通知\n  Future<NotifySettings> getNotify({required String token}) async {\n    final res = await _dio.get(\n      'https://api-1.exptech.dev/api/v2/notify/$token',\n    );\n    return NotifySettings.fromJson(\n      (res.data as List).map((e) => e as int).toList(),\n    );\n  }\n\n  /// 設定通知\n  Future<NotifySettings> setNotify({\n    required String token,\n    required NotifyChannel channel,\n    required Enum status,\n  }) async {\n    if (token.isEmpty) throw ArgumentError.value(token, 'token', 'Token is empty');\n\n    if (status is! EewNotifyType &&\n        status is! EarthquakeNotifyType &&\n        status is! WeatherNotifyType &&\n        status is! TsunamiNotifyType &&\n        status is! BasicNotifyType) {\n      throw ArgumentError.value(\n        status,\n        'status',\n        'Invalid status, must be one of EewNotifyType, EarthquakeNotifyType, WeatherNotifyType, TsunamiNotifyType, or BasicNotifyType',\n      );\n    }\n\n    final res = await _dio.get(\n      'https://api-1.exptech.dev/api/v2/notify/$token/${channel.index}/${status.index}',\n    );\n    return NotifySettings.fromJson(\n      (res.data as List).map((e) => e as int).toList(),\n    );\n  }\n\n  /// Reports network diagnostics to the server.\n  Future<void> sendNetWorkInfo({\n    required String? ip,\n    required String? isp,\n    required List<int?> status,\n    required List<int?> status_dev,\n  }) async {\n    await _dio.post(\n      '${lb}/v1/dpip/networkInfo',\n      data: {'ip': ip, 'isp': isp, 'status': status, 'status_dev': status_dev},\n    );\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/earthquake.dart",
    "content": "part of '../exptech.dart';\n\n/// Earthquake, EEW, and RTS endpoint methods.\nmixin EarthquakeEndpoints {\n  /// Fetches a full earthquake report by [reportId].\n  Future<EarthquakeReport> getReport(String reportId) async {\n    final res = await _dio.get(\n      'https://api.core.exptech.dev/api/v2/eq/report/$reportId',\n    );\n    return EarthquakeReport.fromMap(res.data as Map<String, dynamic>);\n  }\n\n  /// Fetches a paginated list of earthquake reports.\n  Future<List<PartialEarthquakeReport>> getReportList({\n    int? limit = 50,\n    int? page = 1,\n    int? minIntensity = 0,\n    int? maxIntensity = 9,\n    int? minMagnitude = 0,\n    int? maxMagnitude = 8,\n    int? minDepth = 0,\n    int? maxDepth = 700,\n  }) async {\n    final res = await _dio.get(\n      'https://api.core.exptech.dev/api/v2/eq/report',\n      queryParameters: {'limit': limit, 'page': page},\n    );\n    return (res.data as List)\n        .map((e) => PartialEarthquakeReport.fromMap(e as Map<String, dynamic>))\n        .toList();\n  }\n\n  /// Fetches the latest RTS data, or data at [time] (ms since epoch).\n  ///\n  /// Throws [Rtsnodata] when [time] is provided but no data exists for that timestamp.\n  Future<Rts> getRts([int? time]) async {\n    final url = time != null\n        ? 'https://api-1.exptech.dev/api/v2/trem/rts/${time ~/ 1000}'\n        : '${lb}/v2/trem/rts';\n    try {\n      final res = await _dio.get(url);\n      final data = res.data;\n\n      if (data is Map<String, dynamic>) {\n        return Rts.fromMap(data);\n      }\n      if (data is String) {\n        final decoded = jsonDecode(data);\n        if (decoded is Map<String, dynamic>) {\n          return Rts.fromMap(decoded);\n        }\n      }\n      throw FormatException('Unexpected RTS format: ${data.runtimeType}');\n    } on DioException catch (e) {\n      if (time != null && e.response?.statusCode == 404) throw const Rtsnodata();\n      rethrow;\n    }\n  }\n\n  /// Fetches the latest EEW list, or data at [time] (ms since epoch).\n  Future<List<Eew>> getEew([int? time]) async {\n    final url = time != null\n        ? 'https://api.core.exptech.dev/api/v2/eq/eew/${time ~/ 1000}'\n        : '${lb}/v2/eq/eew';\n    final res = await _dio.get(url);\n    final eewList = (res.data as List).map(\n      (e) => Eew.fromMap(e as Map<String, dynamic>),\n    );\n    if (Preference.experimentalEewAllSource == true) return eewList.toList();\n    return eewList.where((e) => e.agency == 'cwa').toList();\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/history.dart",
    "content": "part of '../exptech.dart';\n\n/// History, realtime, and event endpoint methods.\nmixin HistoryEndpoints {\n  /// Fetches the realtime event list.\n  Future<List<History>> getRealtime() async {\n    final res = await _cachedDio.get('${api(1)}/v1/dpip/realtime/list');\n    return (res.data as List).map((e) => History.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches the historical event list.\n  Future<List<History>> getHistory() async {\n    final res = await _cachedDio.get('${api(1)}/v1/dpip/history/list');\n    return (res.data as List).map((e) => History.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches realtime events for [region].\n  Future<List<History>> getRealtimeRegion(String region) async {\n    final res = await _cachedDio.get('${api(1)}/v1/dpip/realtime/$region');\n    return (res.data as List).map((e) => History.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches historical events for [region].\n  Future<List<History>> getHistoryRegion(String region) async {\n    final res = await _cachedDio.get('${api(1)}/v1/dpip/history/$region');\n    return (res.data as List).map((e) => History.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches events associated with [id].\n  Future<List<History>> getEvent(String id) async {\n    final res = await _dio.get('${api(1)}/v1/dpip/event/$id');\n    return (res.data as List).map((e) => History.fromMap(e as Map<String, dynamic>)).toList();\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/station.dart",
    "content": "part of '../exptech.dart';\n\n/// Station and radar endpoint methods.\nmixin StationEndpoints {\n  /// Fetches the TREM station map, keyed by station ID.\n  Future<Map<String, Station>> getStations() async {\n    final res = await _cachedDio.get('${api}/v1/trem/station');\n    return (res.data as Map<String, dynamic>).map(\n      (key, value) => MapEntry(key, Station.fromMap(value as Map<String, dynamic>)),\n    );\n  }\n\n  /// Fetches the list of available radar timestamps.\n  Future<List<String>> getRadarList() async {\n    final res = await _cachedDio.get('${api(1)}/v1/tiles/radar/list');\n    return (res.data as List).map((e) => e.toString()).toList();\n  }\n\n  /// Fetches meteor station data for [id].\n  Future<MeteorStation> getMeteorStation(String id) async {\n    final res = await _dio.get('${api(1)}/v2/meteor/station/$id');\n    return MeteorStation.fromMap(res.data as Map<String, dynamic>);\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/tsunami.dart",
    "content": "part of '../exptech.dart';\n\n/// Tsunami endpoint methods.\nmixin TsunamiEndpoints {\n  /// Fetches tsunami data by [id].\n  Future<Tsunami> getTsunami(String id) async {\n    final res = await _dio.get('${api(1)}/v1/tsunami/$id');\n    return Tsunami.fromMap(res.data as Map<String, dynamic>);\n  }\n\n  /// Fetches the list of available tsunami event IDs.\n  Future<List<String>> getTsunamiList() async {\n    final res = await _dio.get('${api(1)}/v1/tsunami/list');\n    return (res.data as List).map((e) => e as String).toList();\n  }\n}\n"
  },
  {
    "path": "lib/api/endpoints/weather.dart",
    "content": "part of '../exptech.dart';\n\n/// Weather, rain, lightning, and typhoon endpoint methods.\nmixin WeatherEndpoints {\n  /// Fetches the list of available weather data timestamps.\n  Future<List<String>> getWeatherList() async {\n    final res = await _dio.get('${api(1)}/v2/meteor/weather/list');\n    return (res.data as List).map((e) => e.toString()).toList();\n  }\n\n  /// Fetches weather station data for [time].\n  Future<List<WeatherStation>> getWeather(String time) async {\n    final res = await _dio.get('${api(1)}/v2/meteor/weather/$time');\n    return (res.data as List)\n        .map((e) => WeatherStation.fromMap(e as Map<String, dynamic>))\n        .toList();\n  }\n\n  /// Fetches realtime weather for the nearest station to ([lat], [lon]).\n  Future<RealtimeWeather> getWeatherRealtimeByCoords(\n    double lat,\n    double lon,\n  ) async {\n    final res = await _cachedDio.get(\n      '${api(1)}/v3/weather/realtime/${lat.toStringAsFixed(2)},${lon.toStringAsFixed(2)}',\n    );\n    return RealtimeWeather.fromMap(res.data as Map<String, dynamic>);\n  }\n\n  /// Fetches weather forecast data for [region].\n  Future<Map<String, dynamic>> getWeatherForecast(String region) async {\n    final res = await _cachedDio.get('${api(1)}/v3/weather/forecast/$region');\n    return res.data as Map<String, dynamic>;\n  }\n\n  /// Fetches the list of available rain data timestamps.\n  Future<List<String>> getRainList() async {\n    final res = await _dio.get('${api(1)}/v2/meteor/rain/list');\n    return (res.data as List).map((e) => e.toString()).toList();\n  }\n\n  /// Fetches rain station data for [time].\n  Future<List<RainStation>> getRain(String time) async {\n    final res = await _dio.get('${api(1)}/v2/meteor/rain/$time');\n    return (res.data as List).map((e) => RainStation.fromMap(e as Map<String, dynamic>)).toList();\n  }\n\n  /// Fetches the list of available typhoon satellite images.\n  Future<List> getTyphoonImagesList() async {\n    final res = await _dio.get('${api(1)}/v2/meteor/typhoon/images/list');\n    return res.data as List;\n  }\n\n  /// Fetches the typhoon track GeoJSON.\n  Future<Map<String, dynamic>> getTyphoonGeojson() async {\n    final res = await _dio.get('${api(1)}/v2/meteor/typhoon/geojson');\n    return res.data as Map<String, dynamic>;\n  }\n\n  /// Fetches the list of available lightning data timestamps.\n  Future<List<String>> getLightningList() async {\n    final res = await _dio.get('${api(1)}/v2/meteor/lightning/list');\n    return (res.data as List).map((e) => e.toString()).toList();\n  }\n\n  /// Fetches lightning strike data for [time].\n  Future<List<Lightning>> getLightning(String time) async {\n    final res = await _dio.get('${api(1)}/v2/meteor/lightning/$time');\n    return (res.data as List).map((e) => Lightning.fromMap(e as Map<String, dynamic>)).toList();\n  }\n}\n"
  },
  {
    "path": "lib/api/exptech.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:dio_cache_interceptor/dio_cache_interceptor.dart';\nimport 'package:dpip/api/model/announcement.dart';\nimport 'package:dpip/api/model/changelog/changelog.dart';\nimport 'package:dpip/api/model/crowdin/localization_progress.dart';\nimport 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/api/model/meteor_station.dart';\nimport 'package:dpip/api/model/notification_record.dart';\nimport 'package:dpip/api/model/notify/notify_settings.dart';\nimport 'package:dpip/api/model/report/earthquake_report.dart';\nimport 'package:dpip/api/model/report/partial_earthquake_report.dart';\nimport 'package:dpip/api/model/rts/rts.dart';\nimport 'package:dpip/api/model/server_status.dart';\nimport 'package:dpip/api/model/station.dart';\nimport 'package:dpip/api/model/tsunami/tsunami.dart';\nimport 'package:dpip/api/model/weather/lightning.dart';\nimport 'package:dpip/api/model/weather/rain.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/api/route.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:option_result/result.dart';\nimport 'package:zstandard/zstandard.dart';\n\npart '_client.dart';\npart 'endpoints/app.dart';\npart 'endpoints/device.dart';\npart 'endpoints/earthquake.dart';\npart 'endpoints/history.dart';\npart 'endpoints/station.dart';\npart 'endpoints/tsunami.dart';\npart 'endpoints/weather.dart';\n\n/// Client for the ExpTech API.\nclass ExpTech\n    with\n        EarthquakeEndpoints,\n        WeatherEndpoints,\n        TsunamiEndpoints,\n        HistoryEndpoints,\n        StationEndpoints,\n        DeviceEndpoints,\n        AppEndpoints {\n  /// Optional API key for authenticated requests.\n  String? apikey;\n\n  /// Creates an [ExpTech] client.\n  ExpTech({this.apikey});\n}\n"
  },
  {
    "path": "lib/api/model/announcement.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'announcement.g.dart';\n\n@JsonSerializable()\nclass Announcement {\n  final int time;\n  final List<int> tags;\n  final String title;\n  final String content;\n  final bool show;\n\n  Announcement({\n    required this.time,\n    required this.tags,\n    required this.title,\n    required this.content,\n    this.show = false,\n  });\n\n  factory Announcement.fromJson(Map<String, dynamic> json) => _$AnnouncementFromJson(json);\n\n  factory Announcement.fromMap(Map<String, dynamic> map) => Announcement.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$AnnouncementToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/changelog/changelog.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'changelog.g.dart';\n\n@JsonSerializable()\nclass GithubRelease {\n  final String url;\n  @JsonKey(name: 'assets_url')\n  final String assetsUrl;\n  @JsonKey(name: 'upload_url')\n  final String uploadUrl;\n  @JsonKey(name: 'html_url')\n  final String htmlUrl;\n  final int id;\n  final GithubUser author;\n  @JsonKey(name: 'node_id')\n  final String nodeId;\n  @JsonKey(name: 'tag_name')\n  final String tagName;\n  @JsonKey(name: 'target_commitish')\n  final String targetCommitish;\n  final String name;\n  final bool draft;\n  final bool prerelease;\n  @JsonKey(name: 'created_at')\n  final String createdAt;\n  @JsonKey(name: 'published_at')\n  final String publishedAt;\n  final List<GithubReleaseAsset> assets;\n  @JsonKey(name: 'tarball_url')\n  final String tarballUrl;\n  @JsonKey(name: 'zipball_url')\n  final String zipballUrl;\n  final String body;\n  final GithubReleaseReactions? reactions;\n\n  GithubRelease({\n    required this.url,\n    required this.assetsUrl,\n    required this.uploadUrl,\n    required this.htmlUrl,\n    required this.id,\n    required this.author,\n    required this.nodeId,\n    required this.tagName,\n    required this.targetCommitish,\n    required this.name,\n    required this.draft,\n    required this.prerelease,\n    required this.createdAt,\n    required this.publishedAt,\n    required this.assets,\n    required this.tarballUrl,\n    required this.zipballUrl,\n    required this.body,\n    this.reactions,\n  });\n\n  factory GithubRelease.fromJson(Map<String, dynamic> json) => _$GithubReleaseFromJson(json);\n\n  factory GithubRelease.fromMap(Map<String, dynamic> map) => GithubRelease.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$GithubReleaseToJson(this);\n}\n\n@JsonSerializable()\nclass GithubReleaseAsset {\n  final String url;\n  @JsonKey(name: 'browser_download_url')\n  final String browserDownloadUrl;\n  final int id;\n  @JsonKey(name: 'node_id')\n  final String nodeId;\n  final String name;\n  final String label;\n  final String state;\n  @JsonKey(name: 'content_type')\n  final String contentType;\n  final int size;\n  @JsonKey(name: 'download_count')\n  final int downloadCount;\n  @JsonKey(name: 'created_at')\n  final String createdAt;\n  @JsonKey(name: 'updated_at')\n  final String updatedAt;\n  final GithubUser uploader;\n\n  GithubReleaseAsset({\n    required this.url,\n    required this.browserDownloadUrl,\n    required this.id,\n    required this.nodeId,\n    required this.name,\n    required this.label,\n    required this.state,\n    required this.contentType,\n    required this.size,\n    required this.downloadCount,\n    required this.createdAt,\n    required this.updatedAt,\n    required this.uploader,\n  });\n\n  factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) =>\n      _$GithubReleaseAssetFromJson(json);\n  Map<String, dynamic> toJson() => _$GithubReleaseAssetToJson(this);\n}\n\n@JsonSerializable()\nclass GithubUser {\n  final String login;\n  final int id;\n  @JsonKey(name: 'node_id')\n  final String nodeId;\n  @JsonKey(name: 'avatar_url')\n  final String avatarUrl;\n  @JsonKey(name: 'gravatar_id')\n  final String gravatarId;\n  final String url;\n  @JsonKey(name: 'html_url')\n  final String htmlUrl;\n  @JsonKey(name: 'followers_url')\n  final String followersUrl;\n  @JsonKey(name: 'following_url')\n  final String followingUrl;\n  @JsonKey(name: 'gists_url')\n  final String gistsUrl;\n  @JsonKey(name: 'starred_url')\n  final String starredUrl;\n  @JsonKey(name: 'subscriptions_url')\n  final String subscriptionsUrl;\n  @JsonKey(name: 'organizations_url')\n  final String organizationsUrl;\n  @JsonKey(name: 'repos_url')\n  final String reposUrl;\n  @JsonKey(name: 'events_url')\n  final String eventsUrl;\n  @JsonKey(name: 'received_events_url')\n  final String receivedEventsUrl;\n  final String type;\n  @JsonKey(name: 'user_view_type')\n  final String userViewType;\n  @JsonKey(name: 'site_admin')\n  final bool siteAdmin;\n\n  GithubUser({\n    required this.login,\n    required this.id,\n    required this.nodeId,\n    required this.avatarUrl,\n    required this.gravatarId,\n    required this.url,\n    required this.htmlUrl,\n    required this.followersUrl,\n    required this.followingUrl,\n    required this.gistsUrl,\n    required this.starredUrl,\n    required this.subscriptionsUrl,\n    required this.organizationsUrl,\n    required this.reposUrl,\n    required this.eventsUrl,\n    required this.receivedEventsUrl,\n    required this.type,\n    required this.userViewType,\n    required this.siteAdmin,\n  });\n\n  factory GithubUser.fromJson(Map<String, dynamic> json) => _$GithubUserFromJson(json);\n  Map<String, dynamic> toJson() => _$GithubUserToJson(this);\n}\n\n@JsonSerializable()\nclass GithubReleaseReactions {\n  final String url;\n  @JsonKey(name: 'total_count')\n  final int totalCount;\n  @JsonKey(name: '+1')\n  final int plusOne;\n  @JsonKey(name: '-1')\n  final int minusOne;\n  final int laugh;\n  final int hooray;\n  final int confused;\n  final int heart;\n  final int rocket;\n  final int eyes;\n\n  GithubReleaseReactions({\n    required this.url,\n    required this.totalCount,\n    required this.plusOne,\n    required this.minusOne,\n    required this.laugh,\n    required this.hooray,\n    required this.confused,\n    required this.heart,\n    required this.rocket,\n    required this.eyes,\n  });\n\n  factory GithubReleaseReactions.fromJson(Map<String, dynamic> json) =>\n      _$GithubReleaseReactionsFromJson(json);\n  Map<String, dynamic> toJson() => _$GithubReleaseReactionsToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/crowdin/localization_progress.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'localization_progress.g.dart';\n\n@JsonSerializable()\nclass CrowdinLocalizationProgress {\n  final String id;\n  final String language;\n  final double translation;\n  final double approval;\n\n  const CrowdinLocalizationProgress({\n    required this.id,\n    required this.language,\n    required this.translation,\n    required this.approval,\n  });\n\n  factory CrowdinLocalizationProgress.fromJson(Map<String, dynamic> json) =>\n      _$CrowdinLocalizationProgressFromJson(json);\n\n  factory CrowdinLocalizationProgress.fromMap(Map<String, dynamic> map) =>\n      CrowdinLocalizationProgress.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$CrowdinLocalizationProgressToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/eew.dart",
    "content": "import 'package:dpip/api/model/eew_info.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'eew.g.dart';\n\n@JsonSerializable()\nclass Eew {\n  /// 地震速報來源機關\n  @JsonKey(name: 'author')\n  final String agency;\n\n  /// 地震速報 ID\n  final String id;\n\n  /// 地震速報報號\n  final int serial;\n\n  /// 地震速報狀態\n  final int status;\n\n  /// 地震速報是否為最終報\n  @JsonKey(name: 'final', fromJson: parseBoolishInt)\n  final bool isFinal;\n\n  /// 地震速報參數\n  @JsonKey(name: 'eq')\n  final EewInfo info;\n\n  const Eew({\n    required this.agency,\n    required this.id,\n    required this.serial,\n    required this.status,\n    required this.isFinal,\n    required this.info,\n  });\n\n  factory Eew.fromJson(Map<String, dynamic> json) => _$EewFromJson(json);\n\n  factory Eew.fromMap(Map<String, dynamic> map) => Eew.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$EewToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/eew_info.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\npart 'eew_info.g.dart';\n\n@JsonSerializable()\nclass EewInfo {\n  /// 地震速報時間\n  final int time;\n\n  /// 地震震央預估經度\n  @JsonKey(name: 'lon')\n  final double longitude;\n\n  /// 地震震央預估緯度\n  @JsonKey(name: 'lat')\n  final double latitude;\n\n  /// 地震預估深度\n  final double depth;\n\n  /// 地震預估芮氏規模\n  @JsonKey(name: 'mag')\n  final double magnitude;\n\n  /// 地震預估位置\n  @JsonKey(name: 'loc')\n  final String location;\n\n  /// 地震預估最大震度\n  final int max;\n\n  const EewInfo({\n    required this.time,\n    required this.longitude,\n    required this.latitude,\n    required this.depth,\n    required this.magnitude,\n    required this.location,\n    required this.max,\n  });\n\n  LatLng get latlng => LatLng(latitude, longitude);\n\n  factory EewInfo.fromJson(Map<String, dynamic> json) => _$EewInfoFromJson(json);\n\n  Map<String, dynamic> toJson() => _$EewInfoToJson(this);\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is EewInfo &&\n        other.time == time &&\n        other.longitude == longitude &&\n        other.latitude == latitude &&\n        other.depth == depth &&\n        other.magnitude == magnitude &&\n        other.location == location &&\n        other.max == max;\n  }\n\n  @override\n  int get hashCode {\n    return Object.hash(\n      time,\n      longitude,\n      latitude,\n      depth,\n      magnitude,\n      location,\n      max,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/api/model/history/history.dart",
    "content": "import 'package:dpip/api/model/history/intensity_history.dart';\nimport 'package:dpip/api/model/history/report_history.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:timezone/timezone.dart';\n\npart 'history.g.dart';\n\nenum HistoryType {\n  @JsonValue('earthquake')\n  earthquake,\n\n  @JsonValue('intensity')\n  intensity,\n\n  @JsonValue('thunderstorm')\n  thunderstorm,\n\n  @JsonValue('heavy-rain')\n  heavyRain,\n\n  @JsonValue('extremely-heavy-rain')\n  extremelyHeavyRain,\n\n  @JsonValue('torrential-rain')\n  torrentialRain,\n\n  @JsonValue('extremely-torrential-rain')\n  extremelyTorrentialRain,\n\n  @JsonValue('workSchlClos')\n  workAndClassSuspension,\n\n  @JsonValue('seawave')\n  seawave,\n\n  unknown,\n}\n\n@JsonSerializable()\nclass History {\n  final String id;\n  final int status;\n  final HistoryType type;\n  final String icon;\n  final String author;\n  final InfoTime time;\n  final InfoText text;\n  final List<int> area;\n\n  History({\n    required this.id,\n    required this.status,\n    required this.type,\n    required this.icon,\n    required this.author,\n    required this.time,\n    required this.text,\n    required this.area,\n  });\n\n  bool get isExpired {\n    final int? expireTimestamp = time.expires['all'];\n\n    if (expireTimestamp == null) {\n      return false;\n    }\n\n    final TZDateTime expireTimeUTC = expireTimestamp.asTZDateTime;\n    final bool isExpired = TZDateTime.now(UTC).isAfter(expireTimeUTC.toUtc());\n    return isExpired;\n  }\n\n  factory History.fromMap(Map<String, dynamic> map) => History.fromJson(map);\n\n  factory History.fromJson(Map<String, dynamic> json) {\n    HistoryType type;\n    try {\n      type = $enumDecode(_$HistoryTypeEnumMap, json['type']);\n    } catch (e) {\n      // 處理未知類型\n      type = HistoryType.unknown;\n      json['type'] = 'unknown';\n    }\n\n    switch (type) {\n      case HistoryType.earthquake:\n        return ReportHistory.fromJson(json);\n      case HistoryType.intensity:\n        return IntensityHistory.fromJson(json);\n      default:\n        return _$HistoryFromJson(json);\n    }\n  }\n\n  Map<String, dynamic> toJson() => _$HistoryToJson(this);\n}\n\n@JsonSerializable()\nclass InfoTime {\n  @JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)\n  final TZDateTime send;\n  final Map<String, int> expires;\n\n  InfoTime({required this.send, required this.expires});\n\n  TZDateTime get expiresAt {\n    final int expireTimestamp = expires['all']!;\n    return expireTimestamp.asTZDateTime;\n  }\n\n  factory InfoTime.fromJson(Map<String, dynamic> json) => _$InfoTimeFromJson(json);\n\n  Map<String, dynamic> toJson() => _$InfoTimeToJson(this);\n}\n\n@JsonSerializable()\nclass InfoText {\n  final Map<String, InfoTextValue> content;\n  final Map<String, String> description;\n\n  InfoText({required this.content, required this.description});\n\n  factory InfoText.fromJson(Map<String, dynamic> json) => _$InfoTextFromJson(json);\n\n  Map<String, dynamic> toJson() => _$InfoTextToJson(this);\n}\n\n@JsonSerializable()\nclass InfoTextValue {\n  final String title;\n  final String subtitle;\n\n  InfoTextValue({required this.title, required this.subtitle});\n\n  factory InfoTextValue.fromJson(Map<String, dynamic> json) => _$InfoTextValueFromJson(json);\n\n  Map<String, dynamic> toJson() => _$InfoTextValueToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/history/intensity_history.dart",
    "content": "import 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'intensity_history.g.dart';\n\n@JsonSerializable()\nclass IntensityHistoryAddition {\n  final int id;\n  final int serial;\n  final Map<String, List<int>> area;\n  final int max;\n\n  @JsonKey(name: 'final', fromJson: parseBoolishInt)\n  final bool isFinal;\n\n  IntensityHistoryAddition({\n    required this.id,\n    required this.serial,\n    required this.area,\n    required this.max,\n    required this.isFinal,\n  });\n\n  factory IntensityHistoryAddition.fromJson(Map<String, dynamic> json) =>\n      _$IntensityHistoryAdditionFromJson(json);\n\n  Map<String, dynamic> toJson() => _$IntensityHistoryAdditionToJson(this);\n}\n\n@JsonSerializable()\nclass IntensityHistory extends History {\n  final IntensityHistoryAddition addition;\n\n  IntensityHistory({\n    required super.id,\n    required super.status,\n    required super.type,\n    required super.icon,\n    required super.author,\n    required super.time,\n    required super.text,\n    required super.area,\n    required this.addition,\n  });\n\n  factory IntensityHistory.fromJson(Map<String, dynamic> json) => _$IntensityHistoryFromJson(json);\n\n  @override\n  Map<String, dynamic> toJson() => _$IntensityHistoryToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/history/report_history.dart",
    "content": "import 'package:dpip/api/model/history/history.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'report_history.g.dart';\n\n@JsonSerializable()\nclass ReportHistoryAddition {\n  final String id;\n\n  ReportHistoryAddition({required this.id});\n\n  factory ReportHistoryAddition.fromJson(Map<String, dynamic> json) =>\n      _$ReportHistoryAdditionFromJson(json);\n\n  Map<String, dynamic> toJson() => _$ReportHistoryAdditionToJson(this);\n}\n\n@JsonSerializable()\nclass ReportHistory extends History {\n  final ReportHistoryAddition addition;\n\n  ReportHistory({\n    required super.id,\n    required super.status,\n    required super.type,\n    required super.icon,\n    required super.author,\n    required super.time,\n    required super.text,\n    required super.area,\n    required this.addition,\n  });\n\n  factory ReportHistory.fromJson(Map<String, dynamic> json) => _$ReportHistoryFromJson(json);\n\n  @override\n  Map<String, dynamic> toJson() => _$ReportHistoryToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/intensity_listing.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'intensity_listing.g.dart';\n\n@JsonSerializable()\nclass IntensityListing {\n  final String code;\n  final String area;\n  final String station;\n  final int i;\n\n  IntensityListing({\n    required this.code,\n    required this.area,\n    required this.station,\n    required this.i,\n  });\n\n  factory IntensityListing.fromJson(Map<String, dynamic> json) => _$IntensityListingFromJson(json);\n\n  Map<String, dynamic> toJson() => _$IntensityListingToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/location/location.dart",
    "content": "import 'package:dpip/core/i18n.dart';\nimport 'package:dpip/global.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'location.g.dart';\n\nfinal Map<String, String> _cityWithLevelCache = {};\nfinal Map<String, String> _townWithLevelCache = {};\n\n/// Represents a geographical location in Taiwan with administrative hierarchy.\n///\n/// This class encapsulates location data including the city and town names, their administrative levels (縣/市/區/鄉/\n/// 鎮), and geographical coordinates. It provides localized display methods for different presentation formats.\n///\n/// Example:\n/// ```dart\n/// const location = Location(\n///   city: '臺北',\n///   town: '信義',\n///   lat: 25.0330,\n///   lng: 121.5654,\n///   cityLevel: '市',\n///   townLevel: '區',\n/// );\n///\n/// print(location.dynamicName); // \"臺北市 信義區\" or shorter if too long\n/// ```\n@JsonSerializable()\nclass Location {\n  /// The city name (e.g., \"臺北\", \"高雄\").\n  ///\n  /// This represents the primary administrative division in Taiwan's government hierarchy.\n  final String city;\n\n  /// The town/district name (e.g., \"信義\", \"前金\").\n  ///\n  /// This represents the secondary administrative division within a city.\n  final String town;\n\n  /// The latitude coordinate in decimal degrees.\n  ///\n  /// Valid range is approximately 21.5° to 25.5° for Taiwan.\n  final double lat;\n\n  /// The longitude coordinate in decimal degrees.\n  ///\n  /// Valid range is approximately 120° to 122° for Taiwan.\n  final double lng;\n\n  /// The administrative level of the city (e.g., \"市\", \"縣\").\n  ///\n  /// This indicates the type of primary administrative division:\n  /// - \"市\" for special municipalities and provincial cities\n  /// - \"縣\" for counties\n  final String cityLevel;\n\n  /// The administrative level of the town (e.g., \"區\", \"鄉\", \"鎮\").\n  ///\n  /// This indicates the type of secondary administrative division:\n  /// - \"區\" for districts (in cities)\n  /// - \"鄉\" for townships\n  /// - \"鎮\" for towns\n  final String townLevel;\n\n  /// Creates a new [Location] instance.\n  ///\n  /// All parameters are required and represent the administrative and geographical data for a location in Taiwan.\n  const Location({\n    required this.city,\n    required this.town,\n    required this.lat,\n    required this.lng,\n    required this.cityLevel,\n    required this.townLevel,\n  });\n\n  /// Returns the full localized location name with both city and town levels.\n  ///\n  /// Format: `\"{city}{cityLevel} {town}{townLevel}\"`\\\n  /// Example: \"臺北市 信義區\" or \"Taipei City Xinyi District\"\n  ///\n  /// This is the most complete form of the location name including all administrative level indicators.\n  String get cityTownWithLevel => '{city}{cityLevel} {town}{townLevel}'.i18n.args({\n    'city': city.locationName,\n    'cityLevel': cityLevel.locationName,\n    'town': town.locationName,\n    'townLevel': townLevel.locationName,\n  });\n\n  /// Returns the localized location name without administrative levels.\n  ///\n  /// Format: `\"{city} {town}\"`\\\n  /// Example: \"臺北 信義\" or \"Taipei Xinyi\"\n  ///\n  /// This provides a cleaner, shorter display format without the administrative level suffixes.\n  String get cityTown => '{city} {town}'.i18n.args({\n    'city': city.locationName,\n    'town': town.locationName,\n  });\n\n  /// Returns the localized city name with its administrative level.\n  ///\n  /// Format: `\"{city}{cityLevel}\"`\\\n  /// Example: \"臺北市\" or \"Taipei City\"\n  ///\n  /// Use this when you only need to display the primary administrative division.\n  String get cityWithLevel {\n    final key = '$city$cityLevel';\n    return _cityWithLevelCache.putIfAbsent(\n      key,\n      () => '{city}{cityLevel}'.i18n.args({\n        'city': city.locationName,\n        'cityLevel': cityLevel.locationName,\n      }),\n    );\n  }\n\n  /// Returns the localized town name with its administrative level.\n  ///\n  /// Format: `\"{town}{townLevel}\"`\\\n  /// Example: \"信義區\" or \"Xinyi District\"\n  ///\n  /// Use this when you only need to display the secondary administrative division.\n  String get townWithLevel {\n    final key = '$town$townLevel';\n    return _townWithLevelCache.putIfAbsent(\n      key,\n      () => '{town}{townLevel}'.i18n.args({\n        'town': town.locationName,\n        'townLevel': townLevel.locationName,\n      }),\n    );\n  }\n\n  /// Returns a localized location name that adapts to available space.\n  ///\n  /// This method implements a fallback strategy to provide the most appropriate location name based on length\n  /// constraints:\n  ///\n  /// 1. First tries [cityTownWithLevel] (full format)\n  /// 2. If too long (>24 chars), falls back to [townWithLevel]\n  /// 3. If still too long, falls back to just [town] name\n  ///\n  /// This is ideal for UI elements with limited space where you want to show as much location detail as possible while\n  /// maintaining readability.\n  ///\n  /// Example progression:\n  /// - `\"新北市板橋區\"` → `\"板橋區\"` → `\"板橋\"`\n  /// - `\"Qianzhen District, Kaohsiung City\"` → `\"Qianzhen District\"` → `\"Qianzhen\"`\n  String get dynamicName {\n    // Try full format first\n    String content = cityTownWithLevel;\n\n    // Fall back to town with level if too long\n    if (content.length > 24) {\n      content = townWithLevel;\n    }\n\n    // Fall back to town name only if still too long\n    if (content.length > 24) {\n      content = town;\n    }\n\n    return content;\n  }\n\n  /// Creates a [Location] instance from a JSON map.\n  ///\n  /// This factory constructor is generated by `json_annotation` and is used for deserializing location data from JSON\n  /// sources such as APIs or local storage.\n  ///\n  /// Example:\n  /// ```dart\n  /// final json = {\n  ///   'city': '臺北',\n  ///   'town': '信義',\n  ///   'lat': 25.0330,\n  ///   'lng': 121.5654,\n  ///   'cityLevel': '市',\n  ///   'townLevel': '區',\n  /// };\n  /// final location = Location.fromJson(json);\n  /// ```\n  factory Location.fromJson(Map<String, dynamic> json) => _$LocationFromJson(json);\n\n  /// Converts this [Location] instance to a JSON map.\n  ///\n  /// This method is generated by `json_annotation` and is used for serializing location data to JSON format for storage\n  /// or transmission.\n  ///\n  /// Returns a [Map<String, dynamic>] containing all the location properties with their original keys.\n  Map<String, dynamic> toJson() => _$LocationToJson(this);\n\n  /// Attempts to parse a Chinese location string into a [Location] instance.\n  ///\n  /// The input string should be in Chinese and follow the format `\"{city}{cityLevel}{town}{townLevel}\"`, but can have\n  /// variations such as:\n  /// - Missing administrative levels: `\"臺北信義\"` or `\"臺北市信義\"`\n  /// - Extra spaces: `\"臺北市 信義區\"` or `\"臺北 信義\"`\n  /// - Different combinations of levels\n  ///\n  /// Returns `null` if:\n  /// - The city name cannot be found in the location database\n  /// - The town name cannot be found within the identified city\n  /// - The input string is empty or invalid\n  ///\n  /// Examples:\n  /// ```dart\n  /// Location.tryParse(\"臺北市信義區\");     // ✓ Full format\n  /// Location.tryParse(\"臺北信義\");       // ✓ No levels\n  /// Location.tryParse(\"臺北市 信義區\");   // ✓ With spaces\n  /// Location.tryParse(\"臺北信義區\");     // ✓ Mixed levels\n  /// Location.tryParse(\"不存在的地方\");    // ✗ Returns null\n  /// ```\n  ///\n  /// This method searches through the global location map (`Global.location`) to find matching city and town\n  /// combinations.\n  static Location? tryParse(String input) {\n    if (input.trim().isEmpty) return null;\n\n    // Clean the input: remove all spaces and normalize\n    final cleanInput = input.replaceAll(RegExp(r'\\s+'), '').toLowerCase();\n\n    // Normalize traditional/simplified characters for better matching\n    final normalizedInput = _normalizeChineseCharacters(cleanInput);\n\n    // Try to find the best matching location\n    Location? bestMatch;\n    int bestMatchScore = 0;\n\n    // Iterate through all locations in the database\n    for (final locationEntry in Global.location.entries) {\n      final location = locationEntry.value;\n\n      // Generate possible Chinese representations of this location\n      final possibleFormats = [\n        '${location.city}${location.cityLevel}${location.town}${location.townLevel}', // Full format\n        '${location.city}${location.cityLevel}${location.town}', // No town level\n        '${location.city}${location.town}${location.townLevel}', // No city level\n        '${location.city}${location.town}', // No levels\n      ];\n\n      // Check each possible format against the input\n      for (final format in possibleFormats) {\n        final normalizedFormat = _normalizeChineseCharacters(\n          format.toLowerCase(),\n        );\n\n        if (normalizedInput == normalizedFormat) {\n          // Exact match found - return immediately\n          return location;\n        }\n\n        // Calculate match score for partial matches\n        final score = _calculateMatchScore(normalizedInput, normalizedFormat);\n        if (score > bestMatchScore) {\n          bestMatchScore = score;\n          bestMatch = location;\n        }\n      }\n    }\n\n    // Return the best match if it's good enough (threshold: 90% similarity for safety)\n    return bestMatchScore >= 90 ? bestMatch : null;\n  }\n\n  /// Calculates a match score between input and target strings.\n  ///\n  /// Returns a score from 0-100 where 100 is a perfect match.\n  /// Uses a simple similarity algorithm based on common character sequences.\n  static int _calculateMatchScore(String input, String target) {\n    if (input == target) return 100;\n    if (input.isEmpty || target.isEmpty) return 0;\n\n    // Check if input is a substring of target or vice versa\n    if (target.contains(input)) {\n      return (input.length * 100) ~/ target.length;\n    }\n    if (input.contains(target)) {\n      return (target.length * 100) ~/ input.length;\n    }\n\n    // Count matching characters in order\n    int matches = 0;\n    int inputIndex = 0;\n    int targetIndex = 0;\n\n    while (inputIndex < input.length && targetIndex < target.length) {\n      if (input[inputIndex] == target[targetIndex]) {\n        matches++;\n        inputIndex++;\n        targetIndex++;\n      } else {\n        targetIndex++;\n      }\n    }\n\n    return (matches * 100) ~/ input.length;\n  }\n\n  /// Normalizes Chinese characters for better matching between traditional and simplified forms.\n  ///\n  /// This method handles common character variations that might appear in user input\n  /// vs. the database, ensuring \"台中\" matches \"臺中\", etc.\n  static String _normalizeChineseCharacters(String input) {\n    // Map of common simplified -> traditional character pairs used in Taiwan location names\n    const charMap = {\n      '台': '臺', // Taiwan/Platform\n      '县': '縣', // County\n      '区': '區', // District\n      '乡': '鄉', // Township\n      '镇': '鎮', // Town\n      // Add more mappings as needed\n    };\n\n    String result = input;\n    for (final entry in charMap.entries) {\n      result = result.replaceAll(entry.key, entry.value);\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/api/model/meteor_station.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'meteor_station.g.dart';\n\n@JsonSerializable()\nclass MeteorStation {\n  @JsonKey(name: 'station')\n  final MeteorStationInfo station;\n\n  @JsonKey(name: 'time')\n  final List<String> time;\n\n  @JsonKey(name: 'temperature')\n  final List<double> temperature;\n\n  @JsonKey(name: 'wind_speed')\n  final List<double> windSpeed;\n\n  @JsonKey(name: 'precipitation')\n  final List<double> precipitation;\n\n  @JsonKey(name: 'humidity')\n  final List<double> humidity;\n\n  @JsonKey(name: 'pressure')\n  final List<double> pressure;\n\n  @JsonKey(name: 'wind_direction')\n  final List<double> windDirection;\n\n  MeteorStation({\n    required this.station,\n    required this.time,\n    required this.temperature,\n    required this.windSpeed,\n    required this.precipitation,\n    required this.humidity,\n    required this.pressure,\n    required this.windDirection,\n  });\n\n  factory MeteorStation.fromJson(Map<String, dynamic> json) => _$MeteorStationFromJson(json);\n\n  factory MeteorStation.fromMap(Map<String, dynamic> map) => MeteorStation.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$MeteorStationToJson(this);\n}\n\n@JsonSerializable()\nclass MeteorStationInfo {\n  final String name;\n  final String county;\n  final String town;\n  final int altitude;\n  final double lat;\n  final double lng;\n\n  MeteorStationInfo({\n    required this.name,\n    required this.county,\n    required this.town,\n    required this.altitude,\n    required this.lat,\n    required this.lng,\n  });\n\n  factory MeteorStationInfo.fromJson(Map<String, dynamic> json) =>\n      _$MeteorStationInfoFromJson(json);\n  Map<String, dynamic> toJson() => _$MeteorStationInfoToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/notification_record.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'notification_record.g.dart';\n\n@JsonSerializable()\nclass NotificationRecord {\n  final int time;\n  final String title;\n  final String body;\n  final List<String> area;\n  final bool critical;\n\n  NotificationRecord({\n    required this.time,\n    required this.title,\n    required this.body,\n    required this.area,\n    required this.critical,\n  });\n\n  factory NotificationRecord.fromJson(Map<String, dynamic> json) =>\n      _$NotificationRecordFromJson(json);\n\n  factory NotificationRecord.fromMap(Map<String, dynamic> map) => NotificationRecord.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$NotificationRecordToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/notify/notify_settings.dart",
    "content": "import 'package:dpip/models/settings/notify.dart';\n\nclass NotifySettings {\n  final EewNotifyType eew;\n  final EarthquakeNotifyType monitor;\n  final EarthquakeNotifyType report;\n  final EarthquakeNotifyType intensity;\n  final WeatherNotifyType thunderstorm;\n  final WeatherNotifyType weatherAdvisory;\n  final WeatherNotifyType evacuation;\n  final TsunamiNotifyType tsunami;\n  final BasicNotifyType announcement;\n\n  NotifySettings({\n    required this.eew,\n    required this.monitor,\n    required this.report,\n    required this.intensity,\n    required this.thunderstorm,\n    required this.weatherAdvisory,\n    required this.evacuation,\n    required this.tsunami,\n    required this.announcement,\n  });\n\n  factory NotifySettings.fromJson(List<int> json) {\n    return NotifySettings(\n      eew: EewNotifyType.values[json[NotifyChannel.eew.index]],\n      monitor: EarthquakeNotifyType.values[json[NotifyChannel.monitor.index]],\n      report: EarthquakeNotifyType.values[json[NotifyChannel.report.index]],\n      intensity: EarthquakeNotifyType.values[json[NotifyChannel.intensity.index]],\n      thunderstorm: WeatherNotifyType.values[json[NotifyChannel.thunderstorm.index]],\n      weatherAdvisory: WeatherNotifyType.values[json[NotifyChannel.weatherAdvisory.index]],\n      evacuation: WeatherNotifyType.values[json[NotifyChannel.evacuation.index]],\n      tsunami: TsunamiNotifyType.values[json[NotifyChannel.tsunami.index]],\n      announcement: BasicNotifyType.values[json[NotifyChannel.announcement.index]],\n    );\n  }\n\n  List<int> toJson() {\n    return [\n      eew.index,\n      monitor.index,\n      report.index,\n      intensity.index,\n      thunderstorm.index,\n      weatherAdvisory.index,\n      evacuation.index,\n      tsunami.index,\n      announcement.index,\n    ];\n  }\n}\n"
  },
  {
    "path": "lib/api/model/received_notification.dart",
    "content": "class ReceivedNotification {\n  ReceivedNotification({\n    required this.id,\n    required this.title,\n    required this.body,\n    required this.payload,\n  });\n\n  final int id;\n  final String? title;\n  final String? body;\n  final String? payload;\n}\n"
  },
  {
    "path": "lib/api/model/report/area_intensity.dart",
    "content": "import 'package:dpip/api/model/station_intensity.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'area_intensity.g.dart';\n\n@JsonSerializable()\nclass AreaIntensity {\n  /// 區域最大觀測震度\n  @JsonKey(name: 'int')\n  final int intensity;\n\n  /// 區域內測站觀測資料\n  final Map<String, StationIntensity> town;\n\n  AreaIntensity({required this.intensity, required this.town});\n\n  factory AreaIntensity.fromJson(Map<String, dynamic> json) => _$AreaIntensityFromJson(json);\n\n  Map<String, dynamic> toJson() => _$AreaIntensityToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/report/earthquake_report.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:dpip/api/model/report/area_intensity.dart';\nimport 'package:dpip/utils/extensions/iterable.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:timezone/timezone.dart';\n\npart 'earthquake_report.g.dart';\n\n@JsonSerializable()\nclass EarthquakeReport {\n  final String id;\n\n  @JsonKey(name: 'lon')\n  final double longitude;\n\n  @JsonKey(name: 'lat')\n  final double latitude;\n\n  @JsonKey(name: 'loc')\n  final String location;\n\n  final double depth;\n\n  @JsonKey(name: 'mag')\n  final double magnitude;\n\n  final Map<String, AreaIntensity> list;\n\n  @JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)\n  final TZDateTime time;\n\n  final int trem;\n\n  EarthquakeReport({\n    required this.id,\n    required this.longitude,\n    required this.latitude,\n    required this.location,\n    required this.depth,\n    required this.magnitude,\n    required this.list,\n    required this.time,\n    required this.trem,\n  });\n\n  factory EarthquakeReport.fromJson(Map<String, dynamic> json) => _$EarthquakeReportFromJson(json);\n\n  factory EarthquakeReport.fromMap(Map<String, dynamic> map) => EarthquakeReport.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$EarthquakeReportToJson(this);\n\n  String? get number {\n    final n = id.split('-').first;\n\n    if (!n.endsWith('000')) {\n      return n;\n    }\n\n    return null;\n  }\n\n  LatLng get latlng => LatLng(latitude, longitude);\n\n  bool get hasNumber => number != null;\n\n  Uri get reportUrl {\n    final arr = id.split('-');\n    arr.removeAt(0);\n    final mag = '${(magnitude * 10).floor()}';\n\n    if (hasNumber) {\n      final id = number!.substring(3);\n      return Uri.parse(\n        'https://scweb.cwa.gov.tw/zh-tw/earthquake/details/${arr.join()}$mag$id',\n      );\n    }\n\n    return Uri.parse(\n      'https://scweb.cwa.gov.tw/zh-tw/earthquake/details/${arr.join()}$mag',\n    );\n  }\n\n  String get reportImageName {\n    final year = time.year.toString();\n    final month = time.month.toString().padLeft(2, '0');\n    final day = time.day.toString().padLeft(2, '0');\n    final hour = time.hour.toString().padLeft(2, '0');\n    final minute = time.minute.toString().padLeft(2, '0');\n    final second = time.second.toString().padLeft(2, '0');\n    final mag = '${(magnitude * 10).floor()}';\n\n    if (hasNumber) {\n      final id = number!.substring(3);\n      return '$year$month$day$hour$minute$second$mag${id}_H.png';\n    }\n\n    return '$year$month$day$hour$minute$second${mag}_H.png';\n  }\n\n  String get reportImageUrl {\n    final name = reportImageName;\n    final time = name.substring(0, 6);\n    return 'https://scweb.cwa.gov.tw/webdata/OLDEQ/$time/$reportImageName';\n  }\n\n  String? get mapImageBaseName {\n    if (!hasNumber) return null;\n\n    final year = time.year.toString();\n    final id = number!.substring(3);\n\n    return '$year$id';\n  }\n\n  String get traceBaseUrl {\n    final year = time.year.toString();\n    return 'https://scweb.cwa.gov.tw/webdata/drawTrace/plotContour/$year';\n  }\n\n  String? get intensityMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}i.png';\n  }\n\n  String? get intensityMapImageUrl =>\n      intensityMapImageName == null ? null : '$traceBaseUrl/$intensityMapImageName';\n\n  String? get pgaMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}a.png';\n  }\n\n  String? get pgaMapImageUrl => pgaMapImageName == null ? null : '$traceBaseUrl/$pgaMapImageName';\n\n  String? get pgvMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}v.png';\n  }\n\n  String? get pgvMapImageUrl => pgvMapImageName == null ? null : '$traceBaseUrl/$pgvMapImageName';\n\n  LatLngBounds get bounds {\n    final bounds = [latitude, longitude, latitude, longitude];\n\n    for (final area in list.values) {\n      for (final town in area.town.values) {\n        bounds.expandBounds(LatLng(town.lat, town.lon));\n      }\n    }\n\n    return LatLngBounds(\n      southwest: LatLng(bounds[0], bounds[1]),\n      northeast: LatLng(bounds[2], bounds[3]),\n    );\n  }\n\n  int getMaxIntensity() {\n    int max = 0;\n\n    list.forEach((areaName, area) {\n      area.town.forEach((stationName, station) {\n        if (station.intensity > max) max = station.intensity;\n      });\n    });\n\n    return max;\n  }\n\n  String getLocation() {\n    if (location.contains('(')) {\n      return location.substring(\n        location.indexOf('(') + 3,\n        location.indexOf(')'),\n      );\n    } else {\n      return location.substring(0, location.indexOf('方') + 1);\n    }\n  }\n\n  String convertLatLon() {\n    var latFormat = '';\n    var lonFormat = '';\n    var latTemp = latitude;\n    var lonTemp = longitude;\n    if (latTemp > 90) {\n      latTemp = latTemp - 180;\n    }\n    if (lonTemp > 180) {\n      lonTemp = lonTemp - 360;\n    }\n    if (latTemp < 0) {\n      latFormat = '南緯 ${latTemp.abs()} 度';\n    } else {\n      latFormat = '北緯 $latTemp 度';\n    }\n    if (lonTemp < 0) {\n      lonFormat = '西經 ${lonTemp.abs()} 度';\n    } else {\n      lonFormat = '東經 $lonTemp 度';\n    }\n    return '$latFormat　$lonFormat';\n  }\n\n  GeoJsonBuilder toGeoJson() {\n    final stations = list.values.expand((area) => area.town.values);\n    final features = stations\n        .sorted((a, b) => a.intensity.compareTo(b.intensity))\n        .map((station) => station.toGeoJsonFeature())\n        .toList();\n    final cross = GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n      ..setGeometry(latlng.asGeoJsonCooridnate)\n      ..setProperty('icon', 'cross-7')\n      ..setProperty('magnitude', magnitude);\n\n    features.add(cross);\n\n    return GeoJsonBuilder().setFeatures(features);\n  }\n}\n"
  },
  {
    "path": "lib/api/model/report/partial_earthquake_report.dart",
    "content": "import 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:timezone/timezone.dart';\n\npart 'partial_earthquake_report.g.dart';\n\n@JsonSerializable()\nclass PartialEarthquakeReport {\n  final String id;\n\n  @JsonKey(name: 'lon')\n  final double longitude;\n\n  @JsonKey(name: 'lat')\n  final double latitude;\n\n  @JsonKey(name: 'loc')\n  final String location;\n\n  final double depth;\n\n  @JsonKey(name: 'mag')\n  final double magnitude;\n\n  @JsonKey(name: 'int')\n  final int intensity;\n\n  @JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)\n  final TZDateTime time;\n\n  final int trem;\n  final String md5;\n\n  PartialEarthquakeReport({\n    required this.id,\n    required this.longitude,\n    required this.latitude,\n    required this.location,\n    required this.depth,\n    required this.magnitude,\n    required this.intensity,\n    required this.time,\n    required this.trem,\n    required this.md5,\n  });\n\n  factory PartialEarthquakeReport.fromJson(Map<String, dynamic> json) =>\n      _$PartialEarthquakeReportFromJson(json);\n\n  factory PartialEarthquakeReport.fromMap(Map<String, dynamic> map) =>\n      PartialEarthquakeReport.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$PartialEarthquakeReportToJson(this);\n\n  String? get number {\n    final n = id.split('-').first;\n\n    if (!n.endsWith('000')) {\n      return n;\n    }\n\n    return null;\n  }\n\n  LatLng get latlng => LatLng(latitude, longitude);\n\n  bool get hasNumber => number != null;\n\n  Uri get reportUrl {\n    final arr = id.split('-');\n    arr.removeAt(0);\n    final mag = '${(magnitude * 10).floor()}';\n\n    if (hasNumber) {\n      final id = number!.substring(3);\n      return Uri.parse(\n        'https://scweb.cwa.gov.tw/zh-tw/earthquake/details/${arr.join()}$mag$id',\n      );\n    }\n\n    return Uri.parse(\n      'https://scweb.cwa.gov.tw/zh-tw/earthquake/details/${arr.join()}$mag',\n    );\n  }\n\n  String get reportImageName {\n    final year = time.year.toString();\n    final month = time.month.toString().padLeft(2, '0');\n    final day = time.day.toString().padLeft(2, '0');\n    final hour = time.hour.toString().padLeft(2, '0');\n    final minute = time.minute.toString().padLeft(2, '0');\n    final second = time.second.toString().padLeft(2, '0');\n    final mag = '${(magnitude * 10).floor()}';\n\n    if (hasNumber) {\n      final id = number!.substring(3);\n      return '$year$month$day$hour$minute$second$mag${id}_H.png';\n    }\n\n    return '$year$month$day$hour$minute$second${mag}_H.png';\n  }\n\n  String get reportImageUrl {\n    final name = reportImageName;\n    final time = name.substring(0, 6);\n    return 'https://scweb.cwa.gov.tw/webdata/OLDEQ/$time/$reportImageName';\n  }\n\n  String? get mapImageBaseName {\n    if (!hasNumber) return null;\n\n    final year = time.year.toString();\n    final id = number!.substring(3);\n\n    return '$year$id';\n  }\n\n  String get traceBaseUrl {\n    final year = time.year.toString();\n    return 'https://scweb.cwa.gov.tw/webdata/drawTrace/plotContour/$year';\n  }\n\n  String? get intensityMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}i.png';\n  }\n\n  String? get intensityMapImageUrl =>\n      intensityMapImageName == null ? null : '$traceBaseUrl/$intensityMapImageName';\n\n  String? get pgaMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}a.png';\n  }\n\n  String? get pgaMapImageUrl => pgaMapImageName == null ? null : '$traceBaseUrl/$pgaMapImageName';\n\n  String? get pgvMapImageName {\n    if (!hasNumber) return null;\n\n    return '${mapImageBaseName}v.png';\n  }\n\n  String? get pgvMapImageUrl => pgvMapImageName == null ? null : '$traceBaseUrl/$pgvMapImageName';\n\n  String extractLocation() {\n    if (location.contains('(')) {\n      return location.substring(\n        location.indexOf('(') + 3,\n        location.indexOf(')'),\n      );\n    } else {\n      return location.substring(0, location.indexOf('方') + 1);\n    }\n  }\n\n  Color getReportColor() {\n    if (magnitude > 6.5 && intensity >= 7) {\n      return Colors.red;\n    }\n    if (magnitude > 6.0 && intensity >= 5) {\n      return Colors.orange;\n    }\n    if (magnitude > 5.5 && intensity >= 4) {\n      return Colors.yellow;\n    }\n    return Colors.green;\n  }\n\n  GeoJsonFeatureBuilder toGeoJsonFeature() {\n    return GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n        .setId(time.millisecondsSinceEpoch)\n        .setGeometry(latlng.asGeoJsonCooridnate)\n        .setProperty('icon', 'cross-$intensity')\n        .setProperty('magnitude', magnitude)\n        .setProperty('intensity', intensity)\n        .setProperty('time', time.millisecondsSinceEpoch)\n        .setProperty('depth', depth);\n  }\n}\n"
  },
  {
    "path": "lib/api/model/rts/rts.dart",
    "content": "import 'package:dpip/api/model/rts/rts_intensity.dart';\nimport 'package:dpip/api/model/rts/rts_station.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'rts.g.dart';\n\n@JsonSerializable()\nclass Rts {\n  /// 測站地動資料\n  final Map<String, RtsStation> station;\n\n  /// 地動區塊\n  final Map<String, int> box;\n\n  ///資料時間\n  final int time;\n\n  /// 震度列表\n  @JsonKey(name: 'int')\n  final List<RtsIntensity> intensity;\n\n  Rts({\n    required this.station,\n    required this.box,\n    required this.time,\n    required this.intensity,\n  });\n\n  factory Rts.fromJson(Map<String, dynamic> json) => _$RtsFromJson(json);\n\n  factory Rts.fromMap(Map<String, dynamic> map) => Rts.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$RtsToJson(this);\n\n  GeoJsonBuilder toGeoJsonBuilder() => GeoJsonBuilder().setFeatures(\n    station.entries.map((e) {\n      final MapEntry(key: id, value: s) = e;\n\n      final latlng = GlobalProviders.data.station[id]?.info.last.latlng;\n\n      if (latlng == null) {\n        throw Exception('Station info for \"$id\" not found');\n      }\n\n      return GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n        ..setGeometry(latlng.asGeoJsonCooridnate)\n        ..setProperty('id', id)\n        ..setProperty('I', s.I)\n        ..setProperty('i', s.i)\n        ..setProperty('pga', s.pga)\n        ..setProperty('pgv', s.pgv);\n    }),\n  );\n}\n"
  },
  {
    "path": "lib/api/model/rts/rts_intensity.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'rts_intensity.g.dart';\n\n@JsonSerializable()\nclass RtsIntensity {\n  /// 郵遞區號\n  final int code;\n\n  /// 震度\n  final int i;\n\n  RtsIntensity({required this.code, required this.i});\n\n  factory RtsIntensity.fromJson(Map<String, dynamic> json) => _$RtsIntensityFromJson(json);\n\n  Map<String, dynamic> toJson() => _$RtsIntensityToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/rts/rts_station.dart",
    "content": "import 'package:dpip/utils/serialization.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'rts_station.g.dart';\n\n@JsonSerializable()\nclass RtsStation {\n  /// 地動加速度\n  final double pga;\n\n  /// 地動速度\n  final double pgv;\n\n  /// 即時震度\n  final double i;\n\n  /// 衰減震度\n  final double I;\n\n  /// 測站是否觸發\n  @JsonKey(fromJson: parseBoolishInt)\n  final bool? alert;\n\n  RtsStation({\n    required this.pga,\n    required this.pgv,\n    required this.i,\n    required this.I,\n    this.alert,\n  });\n\n  factory RtsStation.fromJson(Map<String, dynamic> json) => _$RtsStationFromJson(json);\n\n  Map<String, dynamic> toJson() => _$RtsStationToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/server_status.dart",
    "content": "import 'package:intl/intl.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'server_status.g.dart';\n\n@JsonSerializable()\nclass ServerStatus {\n  final int time;\n  @JsonKey(name: 'status')\n  final Map<String, ServiceStatus> services;\n\n  ServerStatus({required this.time, required this.services});\n\n  factory ServerStatus.fromJson(Map<String, dynamic> json) => _$ServerStatusFromJson(json);\n\n  factory ServerStatus.fromMap(Map<String, dynamic> map) => ServerStatus.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$ServerStatusToJson(this);\n\n  String get formattedTime {\n    final date = DateTime.fromMillisecondsSinceEpoch(time);\n    return DateFormat('yyyy-MM-dd HH:mm:ss').format(date);\n  }\n}\n\n@JsonSerializable()\nclass ServiceStatus {\n  final int status;\n  final int count;\n\n  ServiceStatus({required this.status, required this.count});\n\n  factory ServiceStatus.fromJson(Map<String, dynamic> json) => _$ServiceStatusFromJson(json);\n\n  Map<String, dynamic> toJson() => _$ServiceStatusToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/station.dart",
    "content": "import 'package:dpip/api/model/station_info.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'station.g.dart';\n\n@JsonSerializable()\nclass Station {\n  /// 測站種類\n  final String net;\n\n  /// 測站資訊\n  final List<StationInfo> info;\n\n  /// 測站是否運作\n  final bool work;\n\n  Station({required this.net, required this.info, required this.work});\n\n  factory Station.fromJson(Map<String, dynamic> json) => _$StationFromJson(json);\n\n  factory Station.fromMap(Map<String, dynamic> map) => Station.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$StationToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/station_info.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\npart 'station_info.g.dart';\n\n@JsonSerializable()\nclass StationInfo {\n  /// 測站郵遞區號 (地區編號)\n  final int code;\n\n  /// 測站經度\n  @JsonKey(name: 'lon')\n  final double longitude;\n\n  /// 測站緯度\n  @JsonKey(name: 'lat')\n  final double latitude;\n\n  /// 測站安裝時間\n  final String time;\n\n  StationInfo({\n    required this.code,\n    required this.longitude,\n    required this.latitude,\n    required this.time,\n  });\n\n  LatLng get latlng => LatLng(latitude, longitude);\n\n  factory StationInfo.fromJson(Map<String, dynamic> json) => _$StationInfoFromJson(json);\n\n  Map<String, dynamic> toJson() => _$StationInfoToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/station_intensity.dart",
    "content": "import 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\npart 'station_intensity.g.dart';\n\n@JsonSerializable()\nclass StationIntensity {\n  /// 測站經度\n  final double lon;\n\n  /// 測站緯度\n  final double lat;\n\n  /// 測站最大觀測震度\n  @JsonKey(name: 'int')\n  final int intensity;\n\n  StationIntensity({\n    required this.lon,\n    required this.lat,\n    required this.intensity,\n  });\n\n  factory StationIntensity.fromJson(Map<String, dynamic> json) => _$StationIntensityFromJson(json);\n\n  LatLng get latlng => LatLng(lat, lon);\n\n  Map<String, dynamic> toJson() => _$StationIntensityToJson(this);\n\n  GeoJsonFeatureBuilder toGeoJsonFeature() {\n    return GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n      ..setGeometry(latlng.asGeoJsonCooridnate)\n      ..setProperty('icon', 'intensity-$intensity');\n  }\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami.dart",
    "content": "import 'package:dpip/api/model/tsunami/tsunami_earthquake.dart';\nimport 'package:dpip/api/model/tsunami/tsunami_info.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'tsunami.g.dart';\n\n@JsonSerializable()\nclass Tsunami {\n  /// - 資料種類\n  ///\n  /// 範例\n  /// ```dart\n  /// \"tsunami\"\n  /// ```\n  final String type;\n\n  /// - 海嘯資訊編號\n  ///\n  /// 範例\n  /// ```dart\n  /// 113003\n  /// ```\n  final int id;\n\n  /// - 海嘯資訊報號\n  ///\n  /// 範例\n  /// ```dart\n  /// 1\n  /// ```\n  final int serial;\n\n  /// - 海嘯資訊發布狀態\n  ///\n  /// 範例\n  /// ```dart\n  /// 0\n  /// ```\n  final int status;\n\n  /// - 海嘯資訊發布單位\n  ///\n  /// 範例\n  /// ```dart\n  /// \"cwa\"\n  /// ```\n  final String author;\n\n  /// - 海嘯資訊報文\n  ///\n  /// 範例\n  /// ```dart\n  /// \"１１３年０４月０３日０７時５８分（臺灣時間），臺灣東部海域發生規模７﹒３地震，震央位於東經１２１﹒６７度、北緯２３﹒７７度。該地震可能引發海嘯影響臺灣，特此發布海嘯警報，提醒沿海地區民眾提高警覺嚴加防範，注意海浪突然湧升所造成的危害。\"\n  /// ```\n  final String content;\n\n  /// - 發布時間\n  final int time;\n\n  /// - 海嘯實測地區編碼\n  final TsunamiEarthquake eq;\n\n  /// - 海嘯實測地區編碼\n  final TsunamiInfo info;\n\n  Tsunami({\n    required this.type,\n    required this.id,\n    required this.serial,\n    required this.status,\n    required this.author,\n    required this.content,\n    required this.time,\n    required this.eq,\n    required this.info,\n  });\n\n  factory Tsunami.fromJson(Map<String, dynamic> json) => _$TsunamiFromJson(json);\n\n  factory Tsunami.fromMap(Map<String, dynamic> map) => Tsunami.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$TsunamiToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami_actual.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'tsunami_actual.g.dart';\n\n@JsonSerializable()\nclass TsunamiActual {\n  /// - 海嘯實測地區\n  ///\n  /// 範例\n  /// ```dart\n  /// \"花蓮\"\n  /// ```\n  final String name;\n\n  /// - 海嘯實測地區編碼\n  ///\n  /// 範例\n  /// ```dart\n  /// \"HL\"\n  /// ```\n  final String id;\n\n  /// - 海嘯實測地區緯度\n  ///\n  /// 範例\n  /// ```dart\n  /// 23.98\n  /// ```\n  final double? lat;\n\n  /// - 海嘯實測地區經度\n  ///\n  /// 範例\n  /// ```dart\n  /// 121.62\n  /// ```\n  final double? lon;\n\n  /// - 海嘯實測高度\n  ///\n  /// 範例\n  /// ```dart\n  /// 27\n  /// ```\n  @JsonKey(name: 'wave_height')\n  final int waveHeight;\n\n  /// - 海嘯實際抵達時間\n  ///\n  /// 範例\n  /// ```dart\n  /// 1712104200000\n  /// ```\n  @JsonKey(name: 'arrival_time')\n  final int arrivalTime;\n\n  TsunamiActual({\n    required this.name,\n    required this.id,\n    required this.lat,\n    required this.lon,\n    required this.waveHeight,\n    required this.arrivalTime,\n  });\n\n  factory TsunamiActual.fromJson(Map<String, dynamic> json) => _$TsunamiActualFromJson(json);\n\n  Map<String, dynamic> toJson() => _$TsunamiActualToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami_earthquake.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'tsunami_earthquake.g.dart';\n\n@JsonSerializable()\nclass TsunamiEarthquake {\n  /// - 地震發生時間\n  ///\n  /// 範例\n  /// ```dart\n  /// \"1712102280000\"\n  /// ```\n  final int time;\n\n  /// - 震央經度\n  ///\n  /// 範例\n  /// ```dart\n  /// 121.67\n  /// ```\n  final double lon;\n\n  /// - 震央緯度\n  ///\n  /// 範例：\n  /// ```dart\n  /// 23.77\n  /// ```\n  final double lat;\n\n  /// - 震源深度\n  ///\n  /// 範例：\n  /// ```dart\n  /// 15.5\n  /// ```\n  final double depth;\n\n  /// - 地震規模\n  ///\n  /// 範例\n  /// ```dart\n  /// 7.3\n  /// ```\n  final double mag;\n\n  /// - 地震位置描述\n  ///\n  /// 範例\n  /// ```dart\n  /// \"花蓮縣中部外海\"\n  /// ```\n  final String loc;\n\n  /// - 地震資訊發布單位\n  ///\n  /// 範例\n  /// ```dart\n  /// \"cwa\"\n  /// ```\n  final String author;\n\n  TsunamiEarthquake({\n    required this.time,\n    required this.lon,\n    required this.lat,\n    required this.depth,\n    required this.mag,\n    required this.loc,\n    required this.author,\n  });\n\n  factory TsunamiEarthquake.fromJson(Map<String, dynamic> json) =>\n      _$TsunamiEarthquakeFromJson(json);\n\n  Map<String, dynamic> toJson() => _$TsunamiEarthquakeToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami_estimate.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'tsunami_estimate.g.dart';\n\n@JsonSerializable()\nclass TsunamiEstimate {\n  /// - 海嘯預警地區\n  ///\n  /// 範例\n  /// ```dart\n  /// \"東部沿海地區\"\n  /// ```\n  final String area;\n\n  /// - 海嘯預估抵達時間\n  ///\n  /// 範例\n  /// ```dart\n  /// 1712102340000\n  /// ```\n  @JsonKey(name: 'arrival_time')\n  final int arrivalTime;\n\n  /// - 海嘯最高預估高度\n  ///\n  /// 範例\n  /// ```dart\n  /// 0\n  /// ```\n  @JsonKey(name: 'wave_height')\n  final int waveHeight;\n\n  TsunamiEstimate({\n    required this.area,\n    required this.arrivalTime,\n    required this.waveHeight,\n  });\n\n  factory TsunamiEstimate.fromJson(Map<String, dynamic> json) => _$TsunamiEstimateFromJson(json);\n\n  Map<String, dynamic> toJson() => _$TsunamiEstimateToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami_info.dart",
    "content": "import 'package:dpip/api/model/tsunami/tsunami_actual.dart';\nimport 'package:dpip/api/model/tsunami/tsunami_estimate.dart';\n\nclass TsunamiInfo {\n  /// - 海嘯資訊資料總類\n  ///   - `estimate` —— 預估資料\n  ///   - `actual` —— 實測資料\n  final String type;\n\n  /// - 海嘯資訊資料\n  final List<dynamic> data;\n\n  TsunamiInfo({required this.type, required this.data});\n\n  factory TsunamiInfo.fromJson(Map<String, dynamic> json) {\n    final type = json['type'] as String;\n    var data = [];\n\n    if (type == 'estimate') {\n      data = (json['data'] as List<dynamic>)\n          .map((item) => TsunamiEstimate.fromJson(item as Map<String, dynamic>))\n          .toList();\n    } else if (type == 'actual') {\n      data = (json['data'] as List<dynamic>)\n          .map((item) => TsunamiActual.fromJson(item as Map<String, dynamic>))\n          .toList();\n    }\n\n    return TsunamiInfo(type: type, data: data);\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'type': type,\n      'data': data.map((item) {\n        if (type == 'estimate') {\n          return (item as TsunamiEstimate).toJson();\n        } else if (type == 'actual') {\n          return (item as TsunamiActual).toJson();\n        }\n        return null;\n      }).toList(),\n    };\n  }\n}\n"
  },
  {
    "path": "lib/api/model/tsunami/tsunami_list.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'tsunami_list.g.dart';\n\n@JsonSerializable()\nclass TsunamiList {\n  final String id;\n\n  TsunamiList({required this.id});\n  factory TsunamiList.fromJson(Map<String, dynamic> json) => _$TsunamiListFromJson(json);\n  Map<String, dynamic> toJson() => _$TsunamiListToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/wave_time.dart",
    "content": "class WaveTime {\n  final double p;\n  final double s;\n\n  const WaveTime({required this.p, required this.s});\n}\n"
  },
  {
    "path": "lib/api/model/weather/lightning.dart",
    "content": "import 'package:dpip/utils/geojson.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'lightning.g.dart';\n\n@JsonSerializable()\nclass Lightning {\n  final int time;\n  final int type;\n  final Location loc;\n\n  const Lightning({required this.time, required this.type, required this.loc});\n\n  factory Lightning.fromJson(Map<String, dynamic> json) => _$LightningFromJson(json);\n\n  factory Lightning.fromMap(Map<String, dynamic> map) => Lightning.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$LightningToJson(this);\n\n  GeoJsonFeatureBuilder<GeoJsonFeatureType> toFeatureBuilder(int currentTime) {\n    final timeDiff = currentTime - time;\n    int level;\n    if (timeDiff < 5 * 60 * 1000) {\n      level = 5;\n    } else if (timeDiff < 10 * 60 * 1000) {\n      level = 10;\n    } else if (timeDiff < 30 * 60 * 1000) {\n      level = 30;\n    } else {\n      level = 60;\n    }\n\n    return GeoJsonFeatureBuilder<GeoJsonFeatureType>(\n      GeoJsonFeatureType.Point,\n    ).setGeometry([loc.lng, loc.lat]).setProperty('type', '$type-$level');\n  }\n}\n\n@JsonSerializable()\nclass Location {\n  final double lat;\n  final double lng;\n\n  const Location({required this.lat, required this.lng});\n\n  factory Location.fromJson(Map<String, dynamic> json) => _$LocationFromJson(json);\n\n  Map<String, dynamic> toJson() => _$LocationToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/weather/rain.dart",
    "content": "import 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/widgets/map/latlng_altitude.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\npart 'rain.g.dart';\n\n@JsonSerializable()\nclass RainStation {\n  final String id;\n\n  final StationInfo station;\n\n  final RainData data;\n\n  const RainStation({\n    required this.id,\n    required this.station,\n    required this.data,\n  });\n\n  factory RainStation.fromJson(Map<String, dynamic> json) => _$RainStationFromJson(json);\n\n  factory RainStation.fromMap(Map<String, dynamic> map) => RainStation.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$RainStationToJson(this);\n\n  GeoJsonFeatureBuilder toFeatureBuilder() => GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n    ..setGeometry(station.latlng.asGeoJsonCooridnate)\n    ..setProperty('id', id)\n    ..setProperty('name', station.name)\n    ..setProperty('county', station.county)\n    ..setProperty('town', station.town)\n    ..setProperty('altitude', station.altitude)\n    ..setProperty('now', data.now)\n    ..setProperty('10m', data.tenMinutes)\n    ..setProperty('1h', data.oneHour)\n    ..setProperty('3h', data.threeHours)\n    ..setProperty('6h', data.sixHours)\n    ..setProperty('12h', data.twelveHours)\n    ..setProperty('24h', data.twentyFourHours)\n    ..setProperty('2d', data.twoDays)\n    ..setProperty('3d', data.threeDays);\n}\n\n@JsonSerializable()\nclass StationInfo {\n  final String name;\n  final String county;\n  final String town;\n  final double altitude;\n  final double lat;\n  final double lng;\n\n  const StationInfo({\n    required this.name,\n    required this.county,\n    required this.town,\n    required this.altitude,\n    required this.lat,\n    required this.lng,\n  });\n\n  factory StationInfo.fromJson(Map<String, dynamic> json) => _$StationInfoFromJson(json);\n\n  Map<String, dynamic> toJson() => _$StationInfoToJson(this);\n\n  LatLng get latlng => LatLng(lat, lng);\n  LatLngAltitude get latlngAltitude => LatLngAltitude(lat, lng, altitude);\n}\n\n@JsonSerializable()\nclass RainData {\n  @JsonKey(name: 'now', defaultValue: 0.0)\n  final double now;\n  @JsonKey(name: '10m', defaultValue: 0.0)\n  final double tenMinutes;\n  @JsonKey(name: '1h', defaultValue: 0.0)\n  final double oneHour;\n  @JsonKey(name: '3h', defaultValue: 0.0)\n  final double threeHours;\n  @JsonKey(name: '6h', defaultValue: 0.0)\n  final double sixHours;\n  @JsonKey(name: '12h', defaultValue: 0.0)\n  final double twelveHours;\n  @JsonKey(name: '24h', defaultValue: 0.0)\n  final double twentyFourHours;\n  @JsonKey(name: '2d', defaultValue: 0.0)\n  final double twoDays;\n  @JsonKey(name: '3d', defaultValue: 0.0)\n  final double threeDays;\n\n  const RainData({\n    required this.now,\n    required this.tenMinutes,\n    required this.oneHour,\n    required this.threeHours,\n    required this.sixHours,\n    required this.twelveHours,\n    required this.twentyFourHours,\n    required this.twoDays,\n    required this.threeDays,\n  });\n\n  factory RainData.fromJson(Map<String, dynamic> json) => _$RainDataFromJson(json);\n\n  Map<String, dynamic> toJson() => _$RainDataToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/weather/weather.dart",
    "content": "import 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:json_annotation/json_annotation.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\npart 'weather.g.dart';\n\n@JsonSerializable()\nclass WeatherStation {\n  String get type => 'weather_station';\n\n  final String id;\n\n  final StationInfo station;\n\n  final WeatherData data;\n\n  final DailyTemperature daily;\n\n  const WeatherStation({\n    required this.id,\n    required this.station,\n    required this.data,\n    required this.daily,\n  });\n\n  factory WeatherStation.fromJson(Map<String, dynamic> json) => _$WeatherStationFromJson(json);\n\n  factory WeatherStation.fromMap(Map<String, dynamic> map) => WeatherStation.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$WeatherStationToJson(this);\n\n  GeoJsonFeatureBuilder toFeatureBuilder() {\n    final direction = (data.wind.direction + 180) % 360;\n    return GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n        .setGeometry(station.latlng.asGeoJsonCooridnate)\n        .setProperty('id', id)\n        .setProperty('name', station.name)\n        .setProperty('county', station.county)\n        .setProperty('town', station.town)\n        .setProperty('temperature', data.air.temperature)\n        .setProperty('relative_humidity', data.air.relativeHumidity)\n        .setProperty('wind_speed', data.wind.speed)\n        .setProperty('wind_direction', direction)\n        .setProperty('icon', switch (data.wind.speed) {\n          < 3.4 => 'wind-1',\n          < 8.0 => 'wind-2',\n          < 13.9 => 'wind-3',\n          < 32.7 => 'wind-4',\n          _ => 'wind-5',\n        });\n  }\n}\n\n@JsonSerializable()\nclass StationInfo {\n  final String name;\n  final String county;\n  final String town;\n  final int altitude;\n  final double lat;\n  final double lng;\n\n  const StationInfo({\n    required this.name,\n    required this.county,\n    required this.town,\n    required this.altitude,\n    required this.lat,\n    required this.lng,\n  });\n\n  factory StationInfo.fromJson(Map<String, dynamic> json) => _$StationInfoFromJson(json);\n\n  Map<String, dynamic> toJson() => _$StationInfoToJson(this);\n\n  LatLng get latlng => LatLng(lat, lng);\n}\n\n@JsonSerializable()\nclass WeatherData {\n  final String weather;\n  final Wind wind;\n  final AirCondition air;\n\n  const WeatherData({\n    required this.weather,\n    required this.wind,\n    required this.air,\n  });\n\n  factory WeatherData.fromJson(Map<String, dynamic> json) => _$WeatherDataFromJson(json);\n\n  Map<String, dynamic> toJson() => _$WeatherDataToJson(this);\n}\n\n@JsonSerializable()\nclass Wind {\n  final int direction;\n  final double speed;\n\n  const Wind({required this.direction, required this.speed});\n\n  factory Wind.fromJson(Map<String, dynamic> json) => _$WindFromJson(json);\n\n  Map<String, dynamic> toJson() => _$WindToJson(this);\n}\n\n@JsonSerializable()\nclass AirCondition {\n  final double temperature;\n  final double pressure;\n  @JsonKey(name: 'relative_humidity')\n  final int relativeHumidity;\n\n  const AirCondition({\n    required this.temperature,\n    required this.pressure,\n    required this.relativeHumidity,\n  });\n\n  factory AirCondition.fromJson(Map<String, dynamic> json) => _$AirConditionFromJson(json);\n\n  Map<String, dynamic> toJson() => _$AirConditionToJson(this);\n}\n\n@JsonSerializable()\nclass DailyTemperature {\n  final TemperatureRecord high;\n  final TemperatureRecord low;\n\n  const DailyTemperature({required this.high, required this.low});\n\n  factory DailyTemperature.fromJson(Map<String, dynamic> json) => _$DailyTemperatureFromJson(json);\n\n  Map<String, dynamic> toJson() => _$DailyTemperatureToJson(this);\n}\n\n@JsonSerializable()\nclass TemperatureRecord {\n  final double temperature;\n  final int time;\n\n  const TemperatureRecord({required this.temperature, required this.time});\n\n  factory TemperatureRecord.fromJson(Map<String, dynamic> json) =>\n      _$TemperatureRecordFromJson(json);\n\n  Map<String, dynamic> toJson() => _$TemperatureRecordToJson(this);\n}\n"
  },
  {
    "path": "lib/api/model/weather_schema.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'weather_schema.g.dart';\n\n@JsonSerializable()\nclass RealtimeWeatherStation {\n  final String name;\n  final double lat;\n  final double lon;\n  final double altitude;\n  final double distance;\n\n  RealtimeWeatherStation({\n    required this.name,\n    required this.lat,\n    required this.lon,\n    required this.altitude,\n    required this.distance,\n  });\n\n  factory RealtimeWeatherStation.fromJson(Map<String, dynamic> json) =>\n      _$RealtimeWeatherStationFromJson(json);\n  Map<String, dynamic> toJson() => _$RealtimeWeatherStationToJson(this);\n}\n\n@JsonSerializable()\nclass RealtimeWeatherWind {\n  final String direction;\n  final double speed;\n  final int beaufort;\n\n  RealtimeWeatherWind({\n    required this.direction,\n    required this.speed,\n    required this.beaufort,\n  });\n\n  factory RealtimeWeatherWind.fromJson(Map<String, dynamic> json) =>\n      _$RealtimeWeatherWindFromJson(json);\n  Map<String, dynamic> toJson() => _$RealtimeWeatherWindToJson(this);\n}\n\n@JsonSerializable()\nclass RealtimeWeatherGust {\n  final double speed;\n  final int beaufort;\n\n  RealtimeWeatherGust({required this.speed, required this.beaufort});\n\n  factory RealtimeWeatherGust.fromJson(Map<String, dynamic> json) =>\n      _$RealtimeWeatherGustFromJson(json);\n  Map<String, dynamic> toJson() => _$RealtimeWeatherGustToJson(this);\n}\n\n@JsonSerializable()\nclass RealtimeWeatherData {\n  final String weather;\n  final int weatherCode;\n  final double temperature;\n  final double humidity;\n  final double rain;\n  final RealtimeWeatherWind wind;\n  final RealtimeWeatherGust gust;\n  final double visibility;\n  final double pressure;\n  final double sunshine;\n\n  RealtimeWeatherData({\n    required this.weather,\n    required this.weatherCode,\n    required this.temperature,\n    required this.humidity,\n    required this.rain,\n    required this.wind,\n    required this.gust,\n    required this.visibility,\n    required this.pressure,\n    required this.sunshine,\n  });\n\n  factory RealtimeWeatherData.fromJson(Map<String, dynamic> json) =>\n      _$RealtimeWeatherDataFromJson(json);\n  Map<String, dynamic> toJson() => _$RealtimeWeatherDataToJson(this);\n}\n\n@JsonSerializable()\nclass RealtimeWeather {\n  final String id;\n  final RealtimeWeatherStation station;\n  final int time;\n  final RealtimeWeatherData data;\n\n  RealtimeWeather({\n    required this.id,\n    required this.station,\n    required this.time,\n    required this.data,\n  });\n\n  factory RealtimeWeather.fromJson(Map<String, dynamic> json) => _$RealtimeWeatherFromJson(json);\n\n  factory RealtimeWeather.fromMap(Map<String, dynamic> map) => RealtimeWeather.fromJson(map);\n\n  Map<String, dynamic> toJson() => _$RealtimeWeatherToJson(this);\n}\n"
  },
  {
    "path": "lib/api/route.dart",
    "content": "import 'dart:math';\n\nclass _BaseUrlGenerator {\n  final String _prefix;\n  final int _count;\n\n  const _BaseUrlGenerator({required String prefix, required int count})\n    : _prefix = prefix,\n      _count = count;\n\n  String call([int? i]) {\n    i ??= Random().nextInt(_count) + 1;\n\n    if (i < 1 || i > _count) {\n      throw ArgumentError.value(\n        i,\n        'i',\n        'Server index must be between 1 and $_count',\n      );\n    }\n\n    return 'https://$_prefix-$i.exptech.dev/api';\n  }\n\n  @override\n  String toString() => call();\n}\n\n/// API base url generator. Use as `'$api/path'` for a random\n/// server, or `'${api(1)}/path'` to pin to a specific server index.\nconst api = _BaseUrlGenerator(prefix: 'api', count: 2);\n\n/// Load-balancer base url generator. Use as `'$lb/path'` for a random\n/// server, or `'${lb(1)}/path'` to pin to a specific server index.\nconst lb = _BaseUrlGenerator(prefix: 'lb', count: 4);\n\n/// Base host for NTP requests.\nString get ntpBase => 'https://lb-${Random().nextInt(4) + 1}.exptech.dev';\n\n/// Base host for radar tile requests.\nString radarTile(String timestamp) =>\n    'https://api-1.exptech.dev/api/v1/tiles/radar/$timestamp/{z}/{x}/{y}.png';\n"
  },
  {
    "path": "lib/app/changelog/_widgets/update_card.dart",
    "content": "/// A card widget that highlights an available app update.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// Displays a summary card for an available update.\n///\n/// Shows [title], [description], and an optional [onViewDetails] callback.\n/// The card uses a gradient derived from the current theme's primary color.\nclass UpdateCard extends StatelessWidget {\n  /// The update title (typically the version name).\n  final String title;\n\n  /// A brief description of what changed in this update.\n  final String description;\n\n  /// Called when the user taps to view full release details.\n  ///\n  /// When `null`, no interactive affordance is shown.\n  final VoidCallback? onViewDetails;\n\n  /// Creates an [UpdateCard] with the given [title] and [description].\n  const UpdateCard({\n    super.key,\n    required this.title,\n    required this.description,\n    this.onViewDetails,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 8,\n      shape: RoundedRectangleBorder(\n        borderRadius: .circular(20),\n      ),\n      child: Container(\n        decoration: BoxDecoration(\n          borderRadius: .circular(20),\n          gradient: LinearGradient(\n            begin: .topLeft,\n            end: .bottomRight,\n            colors: [\n              context.theme.primaryColor.withValues(alpha: 0.1),\n              context.theme.primaryColor.withValues(alpha: 0.3),\n            ],\n          ),\n        ),\n        child: Padding(\n          padding: const .all(20),\n          child: Row(\n            crossAxisAlignment: .start,\n            children: [\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: .start,\n                  children: [\n                    Row(\n                      children: [\n                        Icon(\n                          Icons.new_releases,\n                          size: 28,\n                          color: context.theme.primaryColor,\n                        ),\n                        const SizedBox(width: 10),\n                        Text(\n                          title,\n                          style: context.texts.titleLarge?.copyWith(\n                            fontWeight: .bold,\n                            color: context.theme.primaryColor,\n                          ),\n                        ),\n                      ],\n                    ),\n                    const SizedBox(height: 12),\n                    Text(\n                      description,\n                      style: context.texts.bodyMedium?.copyWith(\n                        color: context.texts.bodyMedium?.color?.withValues(alpha: 0.8),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 20),\n              Container(\n                width: 80,\n                height: 80,\n                decoration: BoxDecoration(\n                  color: Colors.amber.withValues(alpha: 0.2),\n                  shape: .circle,\n                ),\n                child: const Icon(\n                  Icons.update,\n                  size: 48,\n                  color: Colors.amber,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/changelog/page.dart",
    "content": "/// The changelog page, displaying a list of GitHub releases for the app.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/changelog/changelog.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:dpip/widgets/markdown.dart';\nimport 'package:dpip/widgets/typography.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:m3e_collection/m3e_collection.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:option_result/result.dart';\n\n/// Displays the full release changelog fetched from GitHub.\n///\n/// Renders each [GithubRelease] with a sticky header and Markdown body.\n/// Pull-to-refresh triggers a fresh network fetch.\nclass ChangelogPage extends StatefulWidget {\n  /// Creates a [ChangelogPage].\n  const ChangelogPage({super.key});\n\n  @override\n  State<ChangelogPage> createState() => _ChangelogPageState();\n}\n\nclass _ChangelogPageState extends State<ChangelogPage> {\n  final _refreshIndicatorKey = GlobalKey<ExpressiveRefreshIndicatorState>();\n\n  /// The most recently fetched releases result, or `null` while loading.\n  Result<List<GithubRelease>, String>? releases;\n\n  Future<void> _refresh() async {\n    if (_refreshIndicatorKey.currentState case final state?) {\n      state.show();\n    }\n\n    final result = await ExpTech().getReleases();\n    setState(() => releases = result);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: ExpressiveRefreshIndicator.contained(\n        key: _refreshIndicatorKey,\n        backgroundColor: context.colors.primaryContainer,\n        onRefresh: _refresh,\n        edgeOffset: context.padding.top,\n        child: CustomScrollView(\n          slivers: [\n            SliverAppBar(\n              title: Text('更新日誌'.i18n),\n              pinned: true,\n            ),\n            switch (releases) {\n              null => SliverFillRemaining(\n                child: Center(\n                  child: ExpressiveLoadingIndicator(),\n                ),\n              ),\n              Err(:final value) => SliverFillRemaining(\n                child: Center(\n                  child: Column(\n                    mainAxisSize: .min,\n                    spacing: 8,\n                    children: [\n                      ContainedIcon(\n                        Symbols.error_rounded,\n                        color: context.colors.error,\n                        size: 32,\n                        margin: .only(bottom: 8),\n                      ),\n                      TitleText.large(\n                        '發生錯誤'.i18n,\n                        weight: .bold,\n                        align: .center,\n                      ),\n                      BodyText.large(\n                        value,\n                        color: context.colors.onSurfaceVariant,\n                        align: .center,\n                      ),\n                      FilledButton.tonalIcon(\n                        onPressed: _refresh,\n                        icon: Icon(Symbols.refresh_rounded),\n                        label: Text('再試一次'.i18n),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n              Ok(:final value) => SliverMainAxisGroup(\n                slivers: [\n                  for (final (index, release) in value.indexed)\n                    SliverMainAxisGroup(\n                      slivers: [\n                        SliverPersistentHeader(\n                          delegate: _ReleaseHeaderDelegate(release),\n                          pinned: true,\n                        ),\n                        SliverToBoxAdapter(\n                          child: Padding(\n                            padding: const .symmetric(horizontal: 24),\n                            child: Markdown(release.body),\n                          ),\n                        ),\n                        if (index != value.length - 1)\n                          SliverToBoxAdapter(\n                            child: Center(\n                              child: Icon(\n                                Symbols.more_horiz_rounded,\n                                size: 48,\n                                weight: 700,\n                                color: context.colors.outlineVariant,\n                              ),\n                            ),\n                          ),\n                      ],\n                    ),\n                ],\n              ),\n            },\n            SliverPadding(padding: .only(bottom: context.padding.bottom)),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n/// A [SliverPersistentHeaderDelegate] that renders a sticky release header.\n///\n/// Shows the release icon, name, publish date, and a \"current version\" badge\n/// when the release matches the installed app version.\nclass _ReleaseHeaderDelegate extends SliverPersistentHeaderDelegate {\n  /// The release whose metadata is shown in this header.\n  final GithubRelease release;\n\n  /// Creates a [_ReleaseHeaderDelegate] for the given [release].\n  const _ReleaseHeaderDelegate(this.release);\n\n  @override\n  double get minExtent => height;\n\n  @override\n  double get maxExtent => height;\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return Container(\n      height: height,\n      decoration: BoxDecoration(\n        gradient: LinearGradient(\n          stops: [.5, 1],\n          colors: [\n            context.colors.surface,\n            context.colors.surface.withValues(alpha: 0),\n          ],\n          begin: .topCenter,\n          end: .bottomCenter,\n        ),\n      ),\n      padding: .symmetric(horizontal: 24, vertical: 8),\n      child: Row(\n        spacing: 16,\n        children: [\n          ContainedIcon(\n            switch (release.prerelease) {\n              true => Symbols.experiment_rounded,\n              false => Symbols.package_2_rounded,\n            },\n            color: switch (release.prerelease) {\n              true => Colors.orangeAccent,\n              false => Colors.greenAccent,\n            },\n            size: 28,\n          ),\n          Expanded(\n            child: Column(\n              mainAxisSize: .min,\n              crossAxisAlignment: .start,\n              children: [\n                TitleText.large(release.name, weight: .bold),\n                BodyText.medium(\n                  DateTime.parse(\n                    release.publishedAt,\n                  ).toLocaleFullDateString(context),\n                  color: context.colors.onSurfaceVariant,\n                ),\n              ],\n            ),\n          ),\n          if ('v${Global.packageInfo.version}' == release.name)\n            Container(\n              padding: .symmetric(horizontal: 8, vertical: 4),\n              decoration: BoxDecoration(\n                borderRadius: .circular(8),\n                border: .all(color: context.colors.primary),\n                color: context.colors.primaryContainer,\n              ),\n              child: LabelText.large('目前版本'.i18n),\n            ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  bool shouldRebuild(covariant _ReleaseHeaderDelegate oldDelegate) => true;\n\n  /// Fixed height of the header in logical pixels.\n  static const height = kToolbarHeight + 32;\n}\n"
  },
  {
    "path": "lib/app/debug/logs/page.dart",
    "content": "/// The debug logs page, displaying in-app Talker log output.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/material.dart';\nimport 'package:talker_flutter/talker_flutter.dart';\n\n/// Renders the full Talker log screen for in-app debugging.\n///\n/// Theming is derived from the current [BuildContext] color scheme so the\n/// screen respects light/dark mode automatically.\nclass AppDebugLogsPage extends StatelessWidget {\n  /// Creates an [AppDebugLogsPage].\n  const AppDebugLogsPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return TalkerScreen(\n      talker: TalkerManager.instance,\n      appBarTitle: 'App 日誌'.i18n,\n      theme: TalkerScreenTheme(\n        backgroundColor: context.colors.surface,\n        textColor: context.colors.onSurface,\n        cardColor: context.colors.surfaceContainer,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_models/home_location.dart",
    "content": "/// Model for temporarily overriding the home screen location.\nlibrary;\n\nimport 'package:flutter/material.dart';\n\n/// Holds an optional temporary location code for the home screen.\n///\n/// When [temporaryCode] is non-null, the home page uses it instead of the\n/// user's persisted location setting. Call [setTemporaryCode] to update and\n/// notify listeners.\nclass HomeLocationModel extends ChangeNotifier {\n  String? _temporaryCode;\n\n  /// The current temporary location code, or `null` when none is set.\n  String? get temporaryCode => _temporaryCode;\n\n  /// Updates [temporaryCode] to [code] and notifies listeners.\n  ///\n  /// Does nothing if [code] equals the current value.\n  void setTemporaryCode(String? code) {\n    if (_temporaryCode == code) return;\n    _temporaryCode = code;\n    notifyListeners();\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/blurred_button.dart",
    "content": "/// Blurred glass-effect button widgets for the home screen overlay.\nlibrary;\n\nimport 'dart:ui';\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// A full-width text button with a frosted-glass backdrop blur effect.\nclass BlurredTextButton extends StatelessWidget {\n  /// The label displayed inside the button.\n  final String text;\n\n  /// Called when the button is tapped, or `null` to disable the button.\n  final void Function()? onPressed;\n\n  /// Background colour override; defaults to\n  /// [ColorScheme.surfaceContainerHighest] at 60% opacity.\n  final Color? backgroundColor;\n\n  /// Horizontal blur sigma applied to the backdrop.\n  final double sigmaX;\n\n  /// Vertical blur sigma applied to the backdrop.\n  final double sigmaY;\n\n  /// Optional text style applied to the label.\n  final TextStyle? textStyle;\n\n  /// Material elevation for the drop shadow.\n  final double elevation;\n\n  /// Creates a [BlurredTextButton].\n  const BlurredTextButton({\n    super.key,\n    required this.text,\n    required this.onPressed,\n    this.backgroundColor,\n    this.textStyle,\n    this.sigmaX = 8,\n    this.sigmaY = 8,\n    this.elevation = 0,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Colors.transparent,\n      shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n      elevation: elevation,\n      borderRadius: .circular(24),\n      child: ClipRRect(\n        borderRadius: .circular(24),\n        child: BackdropFilter(\n          filter: ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY),\n          child: Container(\n            height: 48,\n            decoration: BoxDecoration(\n              borderRadius: .circular(24),\n              border: Border.all(\n                color: context.colors.outlineVariant.withValues(alpha: 0.4),\n              ),\n              color:\n                  backgroundColor ?? context.colors.surfaceContainerHighest.withValues(alpha: 0.6),\n            ),\n            child: TextButton(\n              style: TextButton.styleFrom(\n                shape: const StadiumBorder(),\n                foregroundColor: context.theme.brightness == .dark\n                    ? const Color.fromARGB(199, 250, 250, 250)\n                    : const Color.fromARGB(255, 50, 50, 50),\n                textStyle: textStyle,\n                padding: const .symmetric(horizontal: 16),\n              ),\n              onPressed: onPressed,\n              child: Text(text),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n/// A circular icon button with a frosted-glass backdrop blur effect.\nclass BlurredIconButton extends StatelessWidget {\n  /// The icon widget rendered inside the button.\n  final Widget icon;\n\n  /// Called when the button is tapped, or `null` to disable the button.\n  final void Function()? onPressed;\n\n  /// Background colour override; defaults to\n  /// [ColorScheme.surfaceContainerHighest] at 60% opacity.\n  final Color? backgroundColor;\n\n  /// Material elevation for the drop shadow.\n  final double elevation;\n\n  /// Horizontal blur sigma applied to the backdrop.\n  final double sigmaX;\n\n  /// Vertical blur sigma applied to the backdrop.\n  final double sigmaY;\n\n  /// Optional tooltip string shown on long-press.\n  final String? tooltip;\n\n  /// Creates a [BlurredIconButton].\n  const BlurredIconButton({\n    super.key,\n    required this.icon,\n    required this.onPressed,\n    this.backgroundColor,\n    this.elevation = 0,\n    this.sigmaX = 8,\n    this.sigmaY = 8,\n    this.tooltip,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 48,\n      height: 48,\n      child: Material(\n        color: Colors.transparent,\n        shape: CircleBorder(\n          side: BorderSide(\n            color: context.colors.outlineVariant.withValues(alpha: 0.4),\n          ),\n        ),\n        elevation: elevation,\n        shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n        clipBehavior: Clip.antiAlias,\n        child: BackdropFilter(\n          filter: ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY),\n          child: IconButton(\n            style: IconButton.styleFrom(\n              backgroundColor:\n                  backgroundColor ?? context.colors.surfaceContainerHighest.withValues(alpha: 0.6),\n              foregroundColor: context.colors.outline,\n            ),\n            onPressed: onPressed,\n            icon: icon,\n            tooltip: tooltip,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/date_timeline_item.dart",
    "content": "/// Timeline item widget that shows a date header with an optional mode selector.\nlibrary;\n\nimport 'package:dpip/app/home/_widgets/mode_toggle_button.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// Renders a date-labelled row in the home history timeline.\n///\n/// When [mode] and [onModeChanged] are provided the label is tappable and\n/// opens a popup menu for switching [HomeMode]. Pass [isOutOfService] as\n/// `true` to restrict available modes to national-only options.\nclass DateTimelineItem extends StatelessWidget {\n  /// The formatted date string displayed in the chip.\n  final String date;\n\n  /// Whether this is the first item in the timeline.\n  final bool first;\n\n  /// Whether this is the last item in the timeline.\n  final bool last;\n\n  /// The currently active [HomeMode], or `null` to hide the mode indicator.\n  final HomeMode? mode;\n\n  /// Called with the newly selected [HomeMode] when the user changes modes.\n  final ValueChanged<HomeMode>? onModeChanged;\n\n  /// When `true`, limits the mode menu to national-only entries.\n  final bool isOutOfService;\n\n  /// Creates a [DateTimelineItem] for the given [date].\n  const DateTimelineItem(\n    this.date, {\n    super.key,\n    this.first = false,\n    this.last = false,\n    this.mode,\n    this.onModeChanged,\n    this.isOutOfService = false,\n  });\n\n  void _showModeMenu(BuildContext context) {\n    if (mode == null || onModeChanged == null) return;\n\n    final RenderBox? button = context.findRenderObject() as RenderBox?;\n    final RenderBox? overlay = context.navigator.overlay?.context.findRenderObject() as RenderBox?;\n\n    if (button == null || overlay == null) return;\n\n    final RelativeRect position = RelativeRect.fromRect(\n      Rect.fromPoints(\n        button.localToGlobal(\n          button.size.bottomLeft(Offset.zero),\n          ancestor: overlay,\n        ),\n        button.localToGlobal(\n          button.size.bottomRight(Offset.zero),\n          ancestor: overlay,\n        ),\n      ),\n      Offset.zero & overlay.size,\n    );\n\n    // 如果在服務區外，只顯示全國模式\n    final availableModes = isOutOfService\n        ? HomeMode.values.where((m) => m.isNational).toList()\n        : HomeMode.values;\n\n    showMenu<HomeMode>(\n      context: context,\n      position: position,\n      shape: RoundedRectangleBorder(borderRadius: .circular(16)),\n      elevation: 8,\n      items: availableModes.map((m) {\n        return PopupMenuItem<HomeMode>(\n          value: m,\n          child: Row(\n            spacing: 12,\n            children: [\n              Icon(\n                m.icon,\n                size: 20,\n                color: mode == m ? context.colors.primary : context.colors.onSurfaceVariant,\n              ),\n              Text(\n                m.label,\n                style: context.texts.bodyMedium?.copyWith(\n                  color: mode == m ? context.colors.primary : context.colors.onSurface,\n                  fontWeight: mode == m ? .bold : .normal,\n                ),\n              ),\n            ],\n          ),\n        );\n      }).toList(),\n    ).then((selectedMode) {\n      if (selectedMode != null && selectedMode != mode) {\n        onModeChanged!(selectedMode);\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return IntrinsicHeight(\n      child: Row(\n        crossAxisAlignment: .stretch,\n        children: [\n          Padding(\n            padding: const .symmetric(horizontal: 16),\n            child: Stack(\n              alignment: .centerLeft,\n              children: [\n                Positioned(\n                  left: 0,\n                  top: first ? 21 : 0,\n                  bottom: last ? null : 0,\n                  height: last ? 21 : null,\n                  child: Stack(\n                    alignment: .center,\n                    children: [\n                      Positioned(\n                        top: 0,\n                        bottom: 0,\n                        width: 1,\n                        child: Container(\n                          color: context.colors.outlineVariant,\n                        ),\n                      ),\n                      SizedBox(\n                        width: 42,\n                        child: Container(\n                          height: 8,\n                          width: 8,\n                          decoration: BoxDecoration(\n                            shape: .circle,\n                            color: context.colors.outlineVariant,\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                Padding(\n                  padding: const .only(top: 16, bottom: 8),\n                  child: InkWell(\n                    onTap: mode != null && onModeChanged != null\n                        ? () => _showModeMenu(context)\n                        : null,\n                    borderRadius: .circular(8),\n                    child: Container(\n                      padding: const .all(8),\n                      decoration: BoxDecoration(\n                        borderRadius: .circular(8),\n                        color: context.colors.secondaryContainer,\n                      ),\n                      child: Row(\n                        mainAxisSize: .min,\n                        spacing: 6,\n                        children: [\n                          if (mode != null) ...[\n                            Icon(\n                              mode!.icon,\n                              size: 16,\n                              color: context.colors.onSecondaryContainer,\n                            ),\n                            Text(\n                              mode!.label,\n                              style: context.texts.labelMedium?.copyWith(\n                                height: 1,\n                                color: context.colors.onSecondaryContainer,\n                                fontWeight: .bold,\n                              ),\n                            ),\n                            Container(\n                              width: 1,\n                              height: 12,\n                              color: context.colors.onSecondaryContainer.withValues(alpha: 0.3),\n                            ),\n                          ],\n                          Text(\n                            date,\n                            style: context.texts.labelLarge?.copyWith(\n                              height: 1,\n                              color: context.colors.onSecondaryContainer,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/eew_card.dart",
    "content": "/// Earthquake Early Warning (EEW) alert card for the home screen.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/core/eew.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_text/styled_text.dart';\n\n/// Displays an EEW alert for the given [data], including magnitude, location,\n/// local intensity estimate, and a live countdown to S-wave arrival.\n///\n/// Tapping the card navigates to the monitor map view.\nclass EewCard extends StatefulWidget {\n  /// The EEW event data to display.\n  final Eew data;\n\n  /// Creates an [EewCard] for the provided [data].\n  const EewCard(this.data, {super.key});\n\n  @override\n  State<EewCard> createState() => _EewCardState();\n}\n\nclass _EewCardState extends State<EewCard> {\n  /// Estimated local seismic intensity, or `null` when location is unavailable.\n  int? localIntensity;\n\n  /// Expected S-wave arrival timestamp in milliseconds, or `null`.\n  int? localArrivalTime;\n\n  /// Current countdown in seconds until S-wave arrival.\n  int countdown = 0;\n\n  Timer? _timer;\n\n  void _updateCountdown() {\n    if (localArrivalTime == null) return;\n\n    final remainingSeconds = ((localArrivalTime! - GlobalProviders.data.currentTime) / 1000)\n        .floor();\n    if (remainingSeconds < -1) return;\n\n    setState(() => countdown = remainingSeconds);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    if (GlobalProviders.location.coordinates != null) {\n      final info = eewLocationInfo(\n        widget.data.info.magnitude,\n        widget.data.info.depth,\n        widget.data.info.latitude,\n        widget.data.info.longitude,\n        GlobalProviders.location.coordinates!.latitude,\n        GlobalProviders.location.coordinates!.longitude,\n      );\n\n      localIntensity = intensityFloatToInt(info.i);\n      localArrivalTime =\n          (widget.data.info.time + sWaveTimeByDistance(widget.data.info.depth, info.dist)).floor();\n    }\n\n    _updateCountdown();\n    _timer = Timer.periodic(\n      const Duration(seconds: 1),\n      (_) => _updateCountdown(),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ResponsiveContainer(\n      maxWidth: 720,\n      child: Stack(\n        children: [\n          IgnorePointer(\n            child: Container(\n              decoration: BoxDecoration(\n                color: context.colors.errorContainer,\n                border: Border.all(color: context.colors.error, width: 2),\n                borderRadius: .circular(16),\n              ),\n              padding: const .all(12),\n              clipBehavior: Clip.antiAlias,\n              child: Column(\n                mainAxisSize: .min,\n                crossAxisAlignment: .start,\n                children: [\n                  Row(\n                    mainAxisAlignment: .spaceBetween,\n                    children: [\n                      Row(\n                        spacing: 8,\n                        children: [\n                          Container(\n                            decoration: BoxDecoration(\n                              color: context.colors.error,\n                              borderRadius: .circular(8),\n                            ),\n                            padding: const .fromLTRB(8, 6, 12, 6),\n                            child: Row(\n                              mainAxisSize: .min,\n                              spacing: 4,\n                              children: [\n                                Icon(\n                                  Symbols.crisis_alert_rounded,\n                                  color: context.colors.onError,\n                                  weight: 700,\n                                  size: 22,\n                                ),\n                                Text(\n                                  'EEW'.i18n,\n                                  style: context.texts.labelLarge!.copyWith(\n                                    color: context.colors.onError,\n                                    fontWeight: .bold,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                          Text(\n                            '第 {serial} 報'.i18n.args({\n                              'serial': widget.data.serial,\n                            }),\n                            style: context.texts.bodyLarge!.copyWith(\n                              color: context.colors.onErrorContainer,\n                            ),\n                          ),\n                        ],\n                      ),\n                      Icon(\n                        Symbols.chevron_right_rounded,\n                        color: context.colors.onErrorContainer,\n                        size: 24,\n                      ),\n                    ],\n                  ),\n                  Padding(\n                    padding: const .only(top: 8),\n                    child: StyledText(\n                      text: localIntensity != null\n                          ? '{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。'\n                                .i18n\n                                .args({\n                                  'time': widget.data.info.time.toSimpleDateTimeString(),\n                                  'location': widget.data.info.location,\n                                  'magnitude': widget.data.info.magnitude.toStringAsFixed(1),\n                                  'intensity': localIntensity!.asIntensityLabel,\n                                })\n                          : '{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。'\n                                .i18n\n                                .args({\n                                  'time': widget.data.info.time.toSimpleDateTimeString(),\n                                  'location': widget.data.info.location,\n                                  'magnitude': widget.data.info.magnitude.toStringAsFixed(1),\n                                  'depth': widget.data.info.depth.toStringAsFixed(1),\n                                }),\n                      style: context.texts.bodyLarge!.copyWith(\n                        color: context.colors.onErrorContainer,\n                      ),\n                      tags: {\n                        'bold': StyledTextTag(\n                          style: const TextStyle(fontWeight: .bold),\n                        ),\n                      },\n                    ),\n                  ),\n                  Selector<SettingsLocationModel, String?>(\n                    selector: (context, model) => model.code,\n                    builder: (context, code, child) {\n                      if (code == null || localIntensity == null) {\n                        return const SizedBox.shrink();\n                      }\n\n                      return Padding(\n                        padding: const .only(top: 8, bottom: 4),\n                        child: IntrinsicHeight(\n                          child: Row(\n                            mainAxisSize: .min,\n                            crossAxisAlignment: .start,\n                            children: [\n                              Expanded(\n                                child: Padding(\n                                  padding: const .all(4),\n                                  child: Column(\n                                    mainAxisSize: .min,\n                                    crossAxisAlignment: .stretch,\n                                    children: [\n                                      Text(\n                                        '所在地預估'.i18n,\n                                        style: context.texts.labelLarge!.copyWith(\n                                          color: context.colors.onErrorContainer.withValues(\n                                            alpha: 0.6,\n                                          ),\n                                        ),\n                                      ),\n                                      Padding(\n                                        padding: const .only(\n                                          top: 12,\n                                          bottom: 8,\n                                        ),\n                                        child: Text(\n                                          localIntensity!.asIntensityLabel,\n                                          style: context.texts.displayMedium!.copyWith(\n                                            fontWeight: .bold,\n                                            color: context.colors.onErrorContainer,\n                                            height: 1,\n                                            leadingDistribution: TextLeadingDistribution.even,\n                                          ),\n                                          textAlign: .center,\n                                        ),\n                                      ),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                              VerticalDivider(\n                                color: context.colors.onErrorContainer.withValues(alpha: 0.4),\n                                width: 24,\n                              ),\n                              Expanded(\n                                child: Padding(\n                                  padding: const .all(4),\n                                  child: Column(\n                                    mainAxisSize: .min,\n                                    crossAxisAlignment: .stretch,\n                                    children: [\n                                      Text(\n                                        '震波'.i18n,\n                                        style: context.texts.labelLarge!.copyWith(\n                                          color: context.colors.onErrorContainer.withValues(\n                                            alpha: 0.6,\n                                          ),\n                                        ),\n                                      ),\n                                      Padding(\n                                        padding: const .only(\n                                          top: 12,\n                                          bottom: 8,\n                                        ),\n                                        child: (countdown > 0)\n                                            ? RichText(\n                                                text: TextSpan(\n                                                  children: [\n                                                    TextSpan(\n                                                      text: countdown.toString(),\n                                                      style: TextStyle(\n                                                        fontSize:\n                                                            context.texts.displayMedium!.fontSize! *\n                                                            1.15,\n                                                      ),\n                                                    ),\n                                                    TextSpan(\n                                                      text: ' 秒'.i18n,\n                                                      style: TextStyle(\n                                                        fontSize:\n                                                            context.texts.labelLarge!.fontSize,\n                                                      ),\n                                                    ),\n                                                  ],\n                                                  style: context.texts.displayMedium!.copyWith(\n                                                    fontWeight: .bold,\n                                                    color: context.colors.onErrorContainer,\n                                                    height: 1,\n                                                    leadingDistribution:\n                                                        TextLeadingDistribution.even,\n                                                  ),\n                                                ),\n                                                textAlign: .center,\n                                              )\n                                            : Text(\n                                                '抵達'.i18n,\n                                                style: context.texts.displayMedium!.copyWith(\n                                                  fontSize:\n                                                      context.texts.displayMedium!.fontSize! * 0.92,\n                                                  fontWeight: .bold,\n                                                  color: context.colors.onErrorContainer,\n                                                  height: 1,\n                                                  leadingDistribution: TextLeadingDistribution.even,\n                                                ),\n                                                textAlign: .center,\n                                              ),\n                                      ),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                            ],\n                          ),\n                        ),\n                      );\n                    },\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Positioned.fill(\n            child: Material(\n              color: Colors.transparent,\n              child: InkWell(\n                onTap: () => MapRoute(layers: 'monitor').push(context),\n                splashColor: context.colors.error.withValues(alpha: 0.2),\n                borderRadius: .circular(16),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _timer?.cancel();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/forecast_card.dart",
    "content": "/// 24-hour weather forecast card for the home screen.\nlibrary;\n\nimport 'dart:math';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:dpip/widgets/typography.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// Renders a horizontal scrolling 24-hour forecast strip from raw [forecast]\n/// JSON data.\n///\n/// Returns [SizedBox.shrink] when the data is missing or malformed.\nclass ForecastCard extends StatelessWidget {\n  /// The raw forecast JSON map returned by the ExpTech API.\n  final Map<String, dynamic> forecast;\n\n  /// Creates a [ForecastCard] with the given raw [forecast] data.\n  const ForecastCard(this.forecast, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    try {\n      final data = forecast['forecast'] as List<dynamic>?;\n      if (data == null || data.isEmpty) return const SizedBox.shrink();\n\n      double minTemp = double.infinity;\n      double maxTemp = double.negativeInfinity;\n      for (final item in data) {\n        final temp = (item['temperature'] as num?)?.toDouble() ?? 0.0;\n        minTemp = min(minTemp, temp);\n        maxTemp = max(maxTemp, temp);\n      }\n\n      return ResponsiveContainer(\n        child: Card(\n          margin: const .symmetric(horizontal: 16, vertical: 8),\n          elevation: 0,\n          shape: RoundedRectangleBorder(\n            borderRadius: .circular(16),\n            side: BorderSide(\n              color: context.colors.outline.withValues(alpha: 0.1),\n            ),\n          ),\n          child: Padding(\n            padding: const .symmetric(vertical: 8),\n            child: Column(\n              mainAxisSize: .min,\n              crossAxisAlignment: .start,\n              children: [\n                Padding(\n                  padding: const .symmetric(horizontal: 12, vertical: 4),\n                  child: Row(\n                    spacing: 8,\n                    children: [\n                      ContainedIcon(\n                        Symbols.weather_mix_rounded,\n                        color: context.colors.primary,\n                        size: 18,\n                      ),\n                      TitleText.medium(\n                        '天氣預報(24h)'.i18n,\n                        weight: .bold,\n                      ),\n                    ],\n                  ),\n                ),\n                SizedBox(\n                  height: 220,\n                  child: ListView.builder(\n                    scrollDirection: .horizontal,\n                    padding: const .all(4),\n                    itemCount: data.length,\n                    itemBuilder: (context, index) {\n                      final item = data[index] as Map<String, dynamic>;\n                      return _ForecastItem(\n                        item: item,\n                        minTemp: minTemp,\n                        maxTemp: maxTemp,\n                      );\n                    },\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error('Failed to render forecast card', e, s);\n    }\n    return const SizedBox.shrink();\n  }\n}\n\n/// A single column in the forecast strip representing one time slot.\nclass _ForecastItem extends StatelessWidget {\n  final Map<String, dynamic> item;\n  final double minTemp;\n  final double maxTemp;\n\n  const _ForecastItem({\n    required this.item,\n    required this.minTemp,\n    required this.maxTemp,\n  });\n\n  static (IconData, Color?) _weatherIcon(String weather) => switch (weather) {\n    final s when s.contains('晴') => (\n      Symbols.sunny_rounded,\n      Colors.orangeAccent,\n    ),\n    final s when s.contains('雨') => (\n      Symbols.rainy_light_rounded,\n      Colors.blueAccent,\n    ),\n    final s when s.contains('雲') || s.contains('陰') => (\n      Symbols.cloud_rounded,\n      Colors.blueGrey[300],\n    ),\n    final s when s.contains('雷') => (\n      Symbols.flash_on_rounded,\n      Colors.yellow,\n    ),\n    final s when s.contains('雪') => (\n      Symbols.snowflake_rounded,\n      Colors.white70,\n    ),\n    _ => (Symbols.wb_cloudy_rounded, Colors.grey.withValues(alpha: 0.6)),\n  };\n\n  @override\n  Widget build(BuildContext context) {\n    final time = item['time'] as String? ?? '';\n    final weather = item['weather'] as String? ?? '';\n    final pop = item['pop'] as int? ?? 0;\n    final temp = (item['temperature'] as num?)?.toDouble() ?? 0.0;\n\n    final tempRange = maxTemp - minTemp;\n    final tempPercent = tempRange > 0\n        ? ((temp - minTemp) / tempRange).clamp(0.0, 1.0) * 0.82 + 0.18\n        : 0.0;\n\n    final (icon, color) = _weatherIcon(weather);\n\n    return Container(\n      width: 52,\n      margin: const .symmetric(horizontal: 2),\n      padding: const .symmetric(vertical: 4),\n      child: Column(\n        spacing: 4,\n        children: [\n          Container(\n            padding: const .all(4),\n            decoration: BoxDecoration(\n              color: context.colors.surfaceContainerHighest.withValues(\n                alpha: 0.5,\n              ),\n              borderRadius: .circular(6),\n            ),\n            child: Icon(icon, color: color, fill: 1, size: 18),\n          ),\n          Row(\n            mainAxisSize: .min,\n            children: [\n              const Icon(\n                Symbols.water_drop_rounded,\n                size: 10,\n                color: Colors.blue,\n              ),\n              Text(\n                '$pop%',\n                style: const TextStyle(\n                  fontSize: 10,\n                  color: Colors.blueAccent,\n                  fontWeight: .w600,\n                ),\n              ),\n            ],\n          ),\n          Expanded(\n            child: Padding(\n              padding: const .symmetric(vertical: 4),\n              child: Column(\n                verticalDirection: .up,\n                children: [\n                  Flexible(\n                    flex: (1 - tempPercent).asPercentage,\n                    child: const SizedBox(),\n                  ),\n                  Flexible(\n                    flex: tempPercent.asPercentage,\n                    child: Container(\n                      width: 16,\n                      decoration: BoxDecoration(\n                        gradient: LinearGradient(\n                          begin: .topCenter,\n                          end: .bottomCenter,\n                          colors: [\n                            context.colors.tertiary,\n                            context.colors.tertiary.withValues(alpha: 0.8),\n                          ],\n                        ),\n                        borderRadius: .circular(16),\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          BodyText.medium(\n            '${temp.round()}°',\n            weight: .bold,\n            color: context.colors.onSurface,\n          ),\n          BodyText.small(\n            time,\n            color: context.colors.onSurfaceVariant,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/hero_weather.dart",
    "content": "/// Full-screen hero weather section displayed at the top of the home page.\nlibrary;\n\nimport 'dart:math';\n\nimport 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// Displays the current temperature and weather condition as a large hero\n/// widget.\n///\n/// Shows a loading skeleton while [isLoading] is `true`, an empty state when\n/// [weather] is `null`, or the full weather content otherwise.\nclass HeroWeather extends StatelessWidget {\n  /// The current weather data, or `null` when unavailable.\n  final RealtimeWeather? weather;\n\n  /// When `true`, shows a loading placeholder instead of weather data.\n  final bool isLoading;\n\n  /// Creates a [HeroWeather] widget.\n  const HeroWeather({\n    super.key,\n    this.weather,\n    this.isLoading = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final screenHeight = context.dimension.height;\n    final statusBarHeight = context.padding.top;\n\n    return SizedBox(\n      height: screenHeight * 0.5,\n      child: Stack(\n        children: [\n          Positioned(\n            top: statusBarHeight + 80,\n            left: 32,\n            right: 32,\n            child: isLoading\n                ? _buildLoadingState(context)\n                : weather != null\n                ? _buildWeatherContent(context)\n                : _buildEmptyState(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildLoadingState(BuildContext context) {\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        Container(\n          width: 100,\n          height: 72,\n          decoration: BoxDecoration(\n            color: Colors.white.withValues(alpha: 0.1),\n            borderRadius: .circular(8),\n          ),\n        ),\n        const SizedBox(height: 8),\n        Container(\n          width: 80,\n          height: 24,\n          decoration: BoxDecoration(\n            color: Colors.white.withValues(alpha: 0.1),\n            borderRadius: .circular(4),\n          ),\n        ),\n        const SizedBox(height: 4),\n        Container(\n          width: 60,\n          height: 16,\n          decoration: BoxDecoration(\n            color: Colors.white.withValues(alpha: 0.1),\n            borderRadius: .circular(4),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildWeatherContent(BuildContext context) {\n    final data = weather!.data;\n    final e =\n        data.humidity / 100 * 6.105 * exp(17.27 * data.temperature / (data.temperature + 237.3));\n    final feelsLike = data.temperature + 0.33 * e - 0.7 * data.wind.speed - 4.0;\n\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        Text(\n          '${data.temperature.round()}°',\n          style: context.texts.displayLarge?.copyWith(\n            fontSize: 72,\n            fontWeight: .w300,\n            color: Colors.white,\n            height: 1,\n            letterSpacing: -2,\n            shadows: [\n              Shadow(\n                color: Colors.black.withValues(alpha: 0.4),\n                blurRadius: 8,\n                offset: const Offset(0, 2),\n              ),\n            ],\n          ),\n        ),\n        const SizedBox(height: 8),\n        Row(\n          mainAxisSize: .min,\n          children: [\n            Icon(\n              _getWeatherIcon(data.weatherCode),\n              size: 24,\n              color: Colors.white.withValues(alpha: 1),\n              shadows: [\n                Shadow(\n                  color: Colors.black.withValues(alpha: 0.3),\n                  blurRadius: 4,\n                  offset: const Offset(0, 1),\n                ),\n              ],\n            ),\n            const SizedBox(width: 8),\n            Text(\n              data.weather,\n              style: context.texts.titleMedium?.copyWith(\n                color: Colors.white.withValues(alpha: 1),\n                fontWeight: .w400,\n                shadows: [\n                  Shadow(\n                    color: Colors.black.withValues(alpha: 0.3),\n                    blurRadius: 4,\n                    offset: const Offset(0, 1),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n        const SizedBox(height: 4),\n        Text(\n          '體感 {feelsLike}°'.i18n.args({\n            'feelsLike': feelsLike.round(),\n          }),\n          style: context.texts.bodyMedium?.copyWith(\n            color: Colors.white.withValues(alpha: 1),\n            shadows: [\n              Shadow(\n                color: Colors.black.withValues(alpha: 0.3),\n                blurRadius: 4,\n                offset: const Offset(0, 1),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildEmptyState(BuildContext context) {\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        Text(\n          '--°',\n          style: context.texts.displayLarge?.copyWith(\n            fontSize: 72,\n            fontWeight: .w300,\n            color: Colors.white.withValues(alpha: 0.5),\n            height: 1,\n            letterSpacing: -2,\n          ),\n        ),\n        const SizedBox(height: 8),\n        Row(\n          mainAxisSize: .min,\n          children: [\n            Icon(\n              Symbols.cloud_off_rounded,\n              size: 24,\n              color: Colors.white.withValues(alpha: 0.5),\n            ),\n            const SizedBox(width: 8),\n            Text(\n              '無天氣資料'.i18n,\n              style: context.texts.titleMedium?.copyWith(\n                color: Colors.white.withValues(alpha: 0.5),\n              ),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  /// Returns the appropriate [IconData] for the given CWA weather [code].\n  IconData _getWeatherIcon(int code) {\n    if (code >= 1 && code <= 3) return Symbols.clear_day_rounded;\n    if (code >= 4 && code <= 7) return Symbols.partly_cloudy_day_rounded;\n    if (code >= 8 && code <= 14) return Symbols.cloud_rounded;\n    if (code >= 15 && code <= 22) return Symbols.rainy_rounded;\n    if (code >= 23 && code <= 28) return Symbols.rainy_heavy_rounded;\n    if (code >= 29 && code <= 35) return Symbols.thunderstorm_rounded;\n    if (code >= 36 && code <= 41) return Symbols.weather_snowy_rounded;\n    if (code >= 42) return Symbols.foggy_rounded;\n\n    return Symbols.cloud_rounded;\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/history_timeline_item.dart",
    "content": "/// Timeline item widget for a single history event entry.\nlibrary;\n\nimport 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/list_icon.dart';\nimport 'package:dpip/widgets/home/event_list_route.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// Renders a single [History] event as a card within the home timeline.\n///\n/// Tapping the card opens the detail view when the event supports it.\n/// Expired events are rendered with reduced opacity to indicate they have\n/// ended.\nclass HistoryTimelineItem extends StatelessWidget {\n  /// The history event to display.\n  final History history;\n\n  /// Whether this is the first item in the timeline group.\n  final bool first;\n\n  /// Whether this is the last item in the timeline group.\n  final bool last;\n\n  /// Whether the event has already expired.\n  final bool expired;\n\n  /// Creates a [HistoryTimelineItem] for the given [history] event.\n  const HistoryTimelineItem({\n    super.key,\n    required this.history,\n    this.first = false,\n    this.last = false,\n    required this.expired,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final hasDetail = shouldShowArrow(history);\n\n    return Container(\n      margin: const .fromLTRB(16, 4, 16, 4),\n      child: Stack(\n        clipBehavior: .none,\n        children: [\n          Positioned(\n            left: 20.5,\n            top: -4,\n            bottom: last ? null : -4,\n            width: 1,\n            child: Container(\n              height: last ? 8 : null,\n              color: context.colors.outlineVariant,\n            ),\n          ),\n          Container(\n            decoration: BoxDecoration(\n              color: context.colors.surfaceContainer,\n              borderRadius: .circular(12),\n            ),\n            child: Material(\n              color: Colors.transparent,\n              child: InkWell(\n                onTap: hasDetail ? () => handleEventList(context, history) : null,\n                borderRadius: .circular(12),\n                child: Padding(\n                  padding: const .all(12),\n                  child: Row(\n                    crossAxisAlignment: .start,\n                    children: [\n                      Container(\n                        height: 40,\n                        width: 40,\n                        decoration: BoxDecoration(\n                          shape: .circle,\n                          color: expired\n                              ? context.colors.surfaceContainerHighest\n                              : context.colors.primaryContainer,\n                        ),\n                        child: Icon(\n                          getListIcon(history.icon),\n                          size: 20,\n                          color: expired\n                              ? context.colors.outline\n                              : context.colors.onPrimaryContainer,\n                        ),\n                      ),\n                      const SizedBox(width: 12),\n                      Expanded(\n                        child: Column(\n                          crossAxisAlignment: .start,\n                          children: [\n                            Text(\n                              DateFormat('HH:mm:ss').format(\n                                history.time.send,\n                              ),\n                              style: context.texts.labelSmall?.copyWith(\n                                color: context.colors.onSurfaceVariant.withValues(\n                                  alpha: expired ? 0.6 : 1,\n                                ),\n                              ),\n                            ),\n                            const SizedBox(height: 2),\n                            Text(\n                              history.text.content['all']!.subtitle,\n                              style: context.texts.titleSmall?.copyWith(\n                                color: context.colors.onSurface.withValues(\n                                  alpha: expired ? 0.6 : 1,\n                                ),\n                                fontWeight: .w600,\n                              ),\n                              maxLines: 1,\n                              overflow: .ellipsis,\n                            ),\n                            const SizedBox(height: 2),\n                            Text(\n                              history.text.description['all']!,\n                              style: context.texts.bodySmall?.copyWith(\n                                color: context.colors.onSurfaceVariant.withValues(\n                                  alpha: expired ? 0.6 : 1,\n                                ),\n                              ),\n                              maxLines: 2,\n                              overflow: .ellipsis,\n                            ),\n                          ],\n                        ),\n                      ),\n                      if (hasDetail)\n                        Padding(\n                          padding: const .only(left: 8),\n                          child: Icon(\n                            Symbols.chevron_right_rounded,\n                            size: 20,\n                            color: context.colors.onSurfaceVariant,\n                          ),\n                        ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/location_button.dart",
    "content": "/// Location selector button and bottom sheet for the home screen.\nlibrary;\n\nimport 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/app/home/_models/home_location.dart';\nimport 'package:dpip/app/home/_widgets/blurred_button.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\n\n/// A frosted-glass text button that shows the current location name and opens\n/// a location-picker sheet on tap.\n///\n/// Uses [HomeLocationModel] to manage a temporary override and\n/// [SettingsLocationModel] for the persisted location code and favourites.\nclass LocationButton extends StatelessWidget {\n  /// Creates a [LocationButton].\n  const LocationButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Consumer2<HomeLocationModel, SettingsLocationModel>(\n      builder: (context, homeLocation, settingsLocation, child) {\n        final savedCode = settingsLocation.code;\n        final favorited = settingsLocation.favorited;\n        final temporaryCode = homeLocation.temporaryCode;\n        final displayCode = temporaryCode ?? savedCode;\n        final location = Global.location[displayCode];\n\n        late String content;\n        if (location == null) {\n          content = '尚未設定'.i18n;\n        } else {\n          content = location.dynamicName;\n        }\n\n        return BlurredTextButton(\n          onPressed: () => _showLocationMenu(\n            context,\n            savedCode: savedCode,\n            temporaryCode: temporaryCode,\n            favorited: favorited,\n          ),\n          text: content,\n          textStyle: context.texts.bodyLarge,\n          elevation: 2,\n        );\n      },\n    );\n  }\n\n  void _showLocationMenu(\n    BuildContext context, {\n    required String? savedCode,\n    required String? temporaryCode,\n    required Set<String> favorited,\n  }) {\n    final currentCode = temporaryCode ?? savedCode;\n    final model = context.read<HomeLocationModel>();\n\n    showModalBottomSheet<String?>(\n      context: context,\n      constraints: context.bottomSheetConstraints,\n      isScrollControlled: true,\n      builder: (sheetContext) => _LocationMenuSheet(\n        savedCode: savedCode,\n        favorited: favorited,\n        currentCode: currentCode,\n        onLocationSelected: (code) {\n          sheetContext.navigator.pop();\n          if (code == savedCode) {\n            model.setTemporaryCode(null);\n          } else {\n            model.setTemporaryCode(code);\n          }\n        },\n        onAddLocationPressed: () {\n          sheetContext.navigator.pop();\n          SettingsLocationSelectRoute().push(context);\n        },\n        onSettingsPressed: () {\n          sheetContext.navigator.pop();\n          SettingsLocationRoute().push(context);\n        },\n      ),\n    );\n  }\n}\n\nclass _LocationMenuSheet extends StatefulWidget {\n  final String? savedCode;\n  final Set<String> favorited;\n  final String? currentCode;\n  final ValueChanged<String> onLocationSelected;\n  final VoidCallback onAddLocationPressed;\n  final VoidCallback onSettingsPressed;\n\n  const _LocationMenuSheet({\n    required this.savedCode,\n    required this.favorited,\n    required this.currentCode,\n    required this.onLocationSelected,\n    required this.onAddLocationPressed,\n    required this.onSettingsPressed,\n  });\n\n  @override\n  State<_LocationMenuSheet> createState() => _LocationMenuSheetState();\n}\n\nclass _LocationMenuSheetState extends State<_LocationMenuSheet> {\n  String? _selectedCity;\n\n  List<Location> get _cities {\n    final Set<String> walked = {};\n    return Global.location.entries\n        .where((e) => walked.add(e.value.cityWithLevel))\n        .map((e) => e.value)\n        .toList();\n  }\n\n  List<MapEntry<String, Location>> get _towns {\n    if (_selectedCity == null) return [];\n    return Global.location.entries.where((e) => e.value.cityWithLevel == _selectedCity).toList();\n  }\n\n  Widget _buildCityList(BuildContext context) {\n    final cities = _cities;\n    final currentLocation = Global.location[widget.currentCode];\n\n    final quickItems = <_QuickItem>[];\n\n    if (widget.savedCode != null && Global.location[widget.savedCode] != null) {\n      final savedLocation = Global.location[widget.savedCode]!;\n      quickItems.add(\n        _QuickItem(\n          code: widget.savedCode!,\n          name: savedLocation.dynamicName,\n          icon: Symbols.home_rounded,\n          isSelected: widget.currentCode == widget.savedCode,\n        ),\n      );\n    }\n\n    for (final code in widget.favorited) {\n      if (code == widget.savedCode) continue;\n      final location = Global.location[code];\n      if (location != null) {\n        quickItems.add(\n          _QuickItem(\n            code: code,\n            name: location.dynamicName,\n            icon: Symbols.star_rounded,\n            isSelected: widget.currentCode == code,\n          ),\n        );\n      }\n    }\n\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        SegmentedList(\n          label: Text('快速切換'.i18n),\n          children: [\n            for (var i = 0; i < quickItems.length; i++)\n              SegmentedListTile(\n                isFirst: i == 0,\n                tileColor: quickItems[i].isSelected ? context.colors.secondaryContainer : null,\n                leading: Icon(\n                  quickItems[i].icon,\n                  fill: 1,\n                  color: quickItems[i].isSelected\n                      ? context.colors.primary\n                      : context.colors.onSurfaceVariant,\n                ),\n                title: Text(\n                  quickItems[i].name,\n                  style: TextStyle(\n                    color: quickItems[i].isSelected\n                        ? context.colors.primary\n                        : context.colors.onSurface,\n                    fontWeight: quickItems[i].isSelected ? .w600 : .normal,\n                  ),\n                ),\n                trailing: quickItems[i].isSelected\n                    ? Icon(\n                        Symbols.check_rounded,\n                        color: context.colors.primary,\n                      )\n                    : null,\n                onTap: () => widget.onLocationSelected(quickItems[i].code),\n              ),\n            SegmentedListTile(\n              isFirst: quickItems.isEmpty,\n              isLast: true,\n              leading: const Icon(Symbols.add_circle_rounded),\n              title: Text('新增地點'.i18n),\n              onTap: widget.onAddLocationPressed,\n            ),\n          ],\n        ),\n\n        SegmentedList.builder(\n          label: Text('選擇縣市'.i18n),\n          itemCount: cities.length,\n          itemBuilder: (context, index) {\n            final city = cities[index];\n\n            return SegmentedListTile(\n              isFirst: index == 0,\n              isLast: index == cities.length - 1,\n              title: Text(city.cityWithLevel),\n              subtitle: currentLocation?.cityWithLevel == city.cityWithLevel\n                  ? Text('目前選擇'.i18n)\n                  : null,\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => setState(\n                () => _selectedCity = city.cityWithLevel,\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget _buildTownList(BuildContext context) {\n    final towns = _towns;\n\n    return SegmentedList.builder(\n      label: Text(_selectedCity!),\n      itemCount: towns.length,\n      itemBuilder: (context, index) {\n        final entry = towns[index];\n        final code = entry.key;\n        final town = entry.value;\n        final isSelected = widget.currentCode == code;\n\n        return SegmentedListTile(\n          isFirst: index == 0,\n          isLast: index == towns.length - 1,\n          tileColor: isSelected ? context.colors.secondaryContainer : null,\n          title: Text(\n            town.townWithLevel,\n            style: TextStyle(\n              color: isSelected ? context.colors.primary : context.colors.onSurface,\n              fontWeight: isSelected ? .w600 : .normal,\n            ),\n          ),\n          trailing: isSelected\n              ? Icon(\n                  Symbols.check_rounded,\n                  color: context.colors.primary,\n                )\n              : null,\n          onTap: () => widget.onLocationSelected(code),\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      borderRadius: const .vertical(top: .circular(16)),\n      clipBehavior: .antiAlias,\n      child: Scaffold(\n        appBar: AppBar(\n          leading: BackButton(\n            onPressed: _selectedCity != null ? () => setState(() => _selectedCity = null) : null,\n          ),\n          title: Text('切換區域'.i18n),\n          centerTitle: true,\n          actions: [\n            IconButton(\n              icon: const Icon(Symbols.settings_rounded),\n              onPressed: widget.onSettingsPressed,\n              tooltip: '位置設定'.i18n,\n            ),\n          ],\n        ),\n        body: SingleChildScrollView(\n          padding: .only(bottom: context.padding.bottom + 16),\n          child: _selectedCity == null ? _buildCityList(context) : _buildTownList(context),\n        ),\n      ),\n    );\n  }\n}\n\nclass _QuickItem {\n  final String code;\n  final String name;\n  final IconData icon;\n  final bool isSelected;\n\n  const _QuickItem({\n    required this.code,\n    required this.name,\n    required this.icon,\n    required this.isSelected,\n  });\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/location_not_set_card.dart",
    "content": "/// Card shown when no home location has been configured.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// Prompts the user to set a home location when none is configured.\n///\n/// Displays an icon, explanatory text, and a button that navigates to the\n/// location settings page.\nclass LocationNotSetCard extends StatelessWidget {\n  /// Creates a [LocationNotSetCard].\n  const LocationNotSetCard({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        color: context.colors.secondaryContainer,\n        border: Border.all(color: context.colors.secondary, width: 2),\n        borderRadius: .circular(16),\n      ),\n      padding: const .only(left: 12, top: 4, right: 4, bottom: 4),\n      clipBehavior: Clip.antiAlias,\n      child: Row(\n        mainAxisSize: .min,\n        mainAxisAlignment: .spaceBetween,\n        children: [\n          Row(\n            spacing: 8,\n            children: [\n              Icon(\n                Symbols.not_listed_location_rounded,\n                color: context.colors.onSecondaryContainer,\n                weight: 500,\n              ),\n              Text(\n                '尚未設定所在地'.i18n,\n                style: context.texts.bodyMedium!.copyWith(\n                  color: context.colors.onSecondaryContainer,\n                ),\n              ),\n            ],\n          ),\n          TextButton(\n            child: Text('設定'.i18n),\n            onPressed: () => SettingsLocationRoute().push(context),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/location_out_of_service.dart",
    "content": "/// Card shown when the user's GPS location is outside the service area.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// Informs the user that their current location is outside the supported\n/// service area (Taiwan).\nclass LocationOutOfServiceCard extends StatelessWidget {\n  /// Creates a [LocationOutOfServiceCard].\n  const LocationOutOfServiceCard({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        color: context.colors.secondaryContainer,\n        border: Border.all(color: context.colors.secondary, width: 2),\n        borderRadius: .circular(16),\n      ),\n      padding: const .all(12),\n      clipBehavior: Clip.antiAlias,\n      child: Row(\n        spacing: 8,\n        children: [\n          Icon(\n            Symbols.wrong_location_rounded,\n            color: context.colors.onSecondaryContainer,\n            weight: 500,\n          ),\n          Text(\n            '服務區域外，僅在臺灣各地可用'.i18n,\n            style: context.texts.bodyMedium!.copyWith(\n              color: context.colors.onSecondaryContainer,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/mode_toggle_button.dart",
    "content": "/// Mode toggle button and [HomeMode] enum for the home screen history filter.\nlibrary;\n\nimport 'dart:ui';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// Describes the four history display modes available on the home screen.\nenum HomeMode {\n  /// Nationwide view showing currently active events.\n  nationalActive,\n\n  /// Nationwide view showing historical events.\n  nationalHistory,\n\n  /// Local view showing currently active events near the user's location.\n  localActive,\n\n  /// Local view showing historical events near the user's location.\n  localHistory,\n}\n\n/// Convenience accessors for [HomeMode] display properties.\nextension HomeModeExtension on HomeMode {\n  /// The localised label shown in the UI for this mode.\n  String get label {\n    switch (this) {\n      case .nationalActive:\n        return '全國 · 生效中'.i18n;\n      case .nationalHistory:\n        return '全國 · 歷史'.i18n;\n      case .localActive:\n        return '所在地 · 生效中'.i18n;\n      case .localHistory:\n        return '所在地 · 歷史'.i18n;\n    }\n  }\n\n  /// The icon that represents this mode.\n  IconData get icon {\n    switch (this) {\n      case .nationalActive:\n      case .nationalHistory:\n        return Symbols.public_rounded;\n      case .localActive:\n      case .localHistory:\n        return Symbols.location_on_rounded;\n    }\n  }\n\n  /// Whether this mode shows nationwide data rather than local data.\n  bool get isNational {\n    return this == .nationalActive || this == .nationalHistory;\n  }\n\n  /// Whether this mode shows currently active events rather than history.\n  bool get isActive {\n    return this == .nationalActive || this == .localActive;\n  }\n}\n\n/// A blurred glass-effect button that opens a popup menu to switch\n/// [HomeMode].\nclass ModeToggleButton extends StatelessWidget {\n  /// The mode currently displayed by the button.\n  final HomeMode currentMode;\n\n  /// Called with the newly selected [HomeMode] when the user makes a choice.\n  final ValueChanged<HomeMode> onModeChanged;\n\n  /// Creates a [ModeToggleButton].\n  const ModeToggleButton({\n    super.key,\n    required this.currentMode,\n    required this.onModeChanged,\n  });\n\n  void _showModeMenu(BuildContext context) {\n    final RenderBox? button = context.findRenderObject() as RenderBox?;\n    final RenderBox? overlay = context.navigator.overlay?.context.findRenderObject() as RenderBox?;\n\n    if (button == null || overlay == null) return;\n\n    final RelativeRect position = RelativeRect.fromRect(\n      Rect.fromPoints(\n        button.localToGlobal(\n          button.size.bottomLeft(Offset.zero),\n          ancestor: overlay,\n        ),\n        button.localToGlobal(\n          button.size.bottomRight(Offset.zero),\n          ancestor: overlay,\n        ),\n      ),\n      Offset.zero & overlay.size,\n    );\n\n    showMenu<HomeMode>(\n      context: context,\n      position: position,\n      shape: RoundedRectangleBorder(borderRadius: .circular(16)),\n      elevation: 8,\n      items: HomeMode.values.map((mode) {\n        return PopupMenuItem<HomeMode>(\n          value: mode,\n          child: Row(\n            spacing: 12,\n            children: [\n              Icon(\n                mode.icon,\n                size: 20,\n                color: currentMode == mode\n                    ? context.colors.primary\n                    : context.colors.onSurfaceVariant,\n              ),\n              Text(\n                mode.label,\n                style: context.texts.bodyMedium?.copyWith(\n                  color: currentMode == mode ? context.colors.primary : context.colors.onSurface,\n                  fontWeight: currentMode == mode ? .bold : .normal,\n                ),\n              ),\n            ],\n          ),\n        );\n      }).toList(),\n    ).then((selectedMode) {\n      if (selectedMode != null && selectedMode != currentMode) {\n        onModeChanged(selectedMode);\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Colors.transparent,\n      shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n      elevation: 2,\n      borderRadius: .circular(24),\n      child: ClipRRect(\n        borderRadius: .circular(24),\n        child: BackdropFilter(\n          filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),\n          child: Container(\n            height: 48,\n            decoration: BoxDecoration(\n              borderRadius: .circular(24),\n              border: Border.all(\n                color: context.colors.outlineVariant.withValues(alpha: 0.4),\n              ),\n              color: context.colors.surfaceContainerHighest.withValues(\n                alpha: 0.6,\n              ),\n            ),\n            child: InkWell(\n              onTap: () => _showModeMenu(context),\n              borderRadius: .circular(24),\n              child: Padding(\n                padding: const .symmetric(horizontal: 16),\n                child: Row(\n                  mainAxisSize: .min,\n                  spacing: 8,\n                  children: [\n                    Icon(\n                      currentMode.icon,\n                      size: 20,\n                      color: context.colors.outline,\n                    ),\n                    Text(\n                      currentMode.label,\n                      style: context.texts.bodyLarge?.copyWith(\n                        color: context.colors.outline,\n                      ),\n                    ),\n                    Icon(\n                      Symbols.arrow_drop_down_rounded,\n                      size: 20,\n                      color: context.colors.outline,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/radar_card.dart",
    "content": "/// Radar map card that shows the latest precipitation radar imagery.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/route.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/maplibre.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/layout.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// An interactive card that embeds a read-only MapLibre radar overlay and\n/// navigates to the full radar map when tapped.\nclass RadarMapCard extends StatefulWidget {\n  /// Creates a [RadarMapCard].\n  const RadarMapCard({super.key});\n\n  @override\n  State<RadarMapCard> createState() => _RadarMapCardState();\n}\n\nclass _RadarMapCardState extends State<RadarMapCard> with WidgetsBindingObserver, RouteAware {\n  MapLibreMapController? _mapController;\n\n  /// Future that resolves to the list of available radar timestamps.\n  late Future<List<String>> radarListFuture;\n\n  Future<void> _setupMapLayers() async {\n    final controller = _mapController;\n    if (controller == null) return;\n\n    final sourceId = MapSourceIds.radar();\n    final layerId = MapLayerIds.radar();\n\n    try {\n      final time = (await radarListFuture).last;\n      final newTileUrl = radarTile(time);\n\n      if (await controller.exists(sourceId, source: true)) {\n        await controller.removeSource(sourceId);\n      }\n\n      await controller.addSource(\n        sourceId,\n        RasterSourceProperties(tiles: [newTileUrl], tileSize: 256),\n      );\n\n      if (!mounted) return;\n\n      if (!await controller.exists(layerId, layer: true)) {\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          const RasterLayerProperties(),\n          belowLayerId: BaseMapLayerIds.exptechCountyOutline,\n        );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapCard._setupMapLayers', e, s);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    radarListFuture = ExpTech().getRadarList();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    final route = ModalRoute.of(context);\n    if (route != null) {\n      routeObserver.subscribe(this, route);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final userLocation = GlobalProviders.location.coordinates;\n    final targetLocation = userLocation ?? DpipMap.kTaiwanCenter;\n    final targetZoom = userLocation != null ? DpipMap.kUserLocationZoom : DpipMap.kTaiwanZoom;\n\n    return ResponsiveContainer(\n      maxWidth: 720,\n      child: Stack(\n        children: [\n          IgnorePointer(\n            child: Container(\n              decoration: BoxDecoration(\n                color: context.colors.surfaceContainer,\n                border: Border.all(color: context.colors.outlineVariant),\n                borderRadius: .circular(16),\n              ),\n              child: ClipRRect(\n                borderRadius: .circular(16),\n                child: Layout.col.min(\n                  children: [\n                    SizedBox(\n                      height: 200,\n                      child: DpipMap(\n                        initialCameraPosition: CameraPosition(\n                          target: targetLocation,\n                          zoom: targetZoom,\n                        ),\n                        onMapCreated: (controller) => _mapController = controller,\n                        onStyleLoadedCallback: () => _setupMapLayers(),\n                        dragEnabled: false,\n                        rotateGesturesEnabled: false,\n                        zoomGesturesEnabled: false,\n                        focusUserLocationWhenUpdated: false,\n                      ),\n                    ),\n                    Padding(\n                      padding: const .symmetric(\n                        horizontal: 16,\n                        vertical: 12,\n                      ),\n                      child: Layout.row.between(\n                        children: [\n                          Layout.row[8](\n                            children: [\n                              const Icon(Symbols.radar_rounded, size: 24),\n                              Text(\n                                '雷達回波'.i18n,\n                                style: context.texts.titleMedium,\n                              ),\n                              FutureBuilder(\n                                future: radarListFuture,\n                                builder: (context, snapshot) {\n                                  final data = snapshot.data;\n\n                                  if (data == null) {\n                                    return const SizedBox.shrink();\n                                  }\n\n                                  final style = context.texts.labelSmall?.copyWith(\n                                    color: context.colors.onSurfaceVariant,\n                                  );\n\n                                  return Container(\n                                    padding: const .symmetric(\n                                      horizontal: 8,\n                                      vertical: 4,\n                                    ),\n                                    decoration: BoxDecoration(\n                                      color: context.colors.surfaceContainer,\n                                      border: Border.all(\n                                        color: context.colors.outlineVariant,\n                                      ),\n                                      borderRadius: .circular(16),\n                                    ),\n                                    child: Layout.row[4](\n                                      children: [\n                                        Icon(\n                                          Symbols.schedule_rounded,\n                                          size: (style?.fontSize ?? 12) * 1.25,\n                                          color: context.colors.onSurfaceVariant,\n                                        ),\n                                        Text(\n                                          data.last.toSimpleDateTimeString(),\n                                          style: style,\n                                        ),\n                                      ],\n                                    ),\n                                  );\n                                },\n                              ),\n                            ],\n                          ),\n                          const Icon(\n                            Symbols.chevron_right_rounded,\n                            size: 24,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          Positioned.fill(\n            child: Material(\n              type: MaterialType.transparency,\n              child: InkWell(\n                onTap: () => MapRoute(layers: 'radar').push(context),\n                borderRadius: .circular(16),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    routeObserver.unsubscribe(this);\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/thunderstorm_card.dart",
    "content": "/// Card that alerts the user to active thunderstorm warnings near their\n/// location.\nlibrary;\n\nimport 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/route/event_viewer/thunderstorm.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:styled_text/styled_text.dart';\n\n/// Displays a real-time thunderstorm alert for the given [history] event.\n///\n/// Tapping the card navigates to the [ThunderstormPage] detail view.\nclass ThunderstormCard extends StatelessWidget {\n  /// The history event that triggered this thunderstorm alert.\n  final History history;\n\n  /// Creates a [ThunderstormCard] for the provided [history].\n  const ThunderstormCard(this.history, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ResponsiveContainer(\n      maxWidth: 720,\n      child: Stack(\n        children: [\n          IgnorePointer(\n            child: Container(\n              decoration: BoxDecoration(\n                color: context.theme.extendedColors.blueContainer,\n                border: Border.all(\n                  color: context.theme.extendedColors.blue,\n                  width: 2,\n                ),\n                borderRadius: .circular(16),\n              ),\n              padding: const .all(12),\n              clipBehavior: Clip.antiAlias,\n              child: Column(\n                mainAxisSize: .min,\n                crossAxisAlignment: .start,\n                children: [\n                  Row(\n                    mainAxisAlignment: .spaceBetween,\n                    children: [\n                      Row(\n                        spacing: 8,\n                        children: [\n                          Container(\n                            decoration: BoxDecoration(\n                              color: context.theme.extendedColors.blue,\n                              borderRadius: .circular(8),\n                            ),\n                            padding: const .fromLTRB(8, 6, 12, 6),\n                            child: Row(\n                              mainAxisSize: .min,\n                              spacing: 4,\n                              children: [\n                                Icon(\n                                  Symbols.thunderstorm_rounded,\n                                  color: context.theme.extendedColors.onBlue,\n                                  weight: 700,\n                                  size: 22,\n                                ),\n                                Text(\n                                  '雷雨即時訊息'.i18n,\n                                  style: context.texts.labelLarge!.copyWith(\n                                    color: context.theme.extendedColors.onBlue,\n                                    fontWeight: .bold,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ],\n                      ),\n                      Icon(\n                        Symbols.chevron_right_rounded,\n                        color: context.colors.onErrorContainer,\n                        size: 24,\n                      ),\n                    ],\n                  ),\n                  Padding(\n                    padding: const .only(top: 8),\n                    child: StyledText(\n                      text: '您所在區域附近有劇烈雷雨或降雨發生，請注意防範，持續至 <bold>{time}</bold> 。'.i18n.args({\n                        'time': history.time.expiresAt.toSimpleDateTimeString(),\n                      }),\n                      style: context.texts.bodyLarge!.copyWith(\n                        color: context.theme.extendedColors.onBlueContainer,\n                      ),\n                      tags: {\n                        'bold': StyledTextTag(\n                          style: const TextStyle(\n                            fontWeight: .bold,\n                          ),\n                        ),\n                      },\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Positioned.fill(\n            child: Material(\n              color: Colors.transparent,\n              child: InkWell(\n                onTap: () => context.navigator.push(\n                  MaterialPageRoute(\n                    builder: (context) => ThunderstormPage(item: history),\n                  ),\n                ),\n                splashColor: context.theme.extendedColors.blue.withValues(\n                  alpha: 0.2,\n                ),\n                borderRadius: .circular(16),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/weather_header.dart",
    "content": "/// Detailed weather header widget showing temperature, conditions, and\n/// station metadata.\nlibrary;\n\nimport 'dart:math';\n\nimport 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/weather_icon.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\nimport 'package:skeletonizer/skeletonizer.dart';\n\n/// Renders a full weather summary for the given [weather] observation,\n/// including temperature, feels-like, condition chips, and station info.\n///\n/// Call [WeatherHeader.skeleton] to show a loading placeholder while data\n/// is being fetched.\nclass WeatherHeader extends StatelessWidget {\n  /// The current realtime weather observation to display.\n  final RealtimeWeather weather;\n\n  /// Creates a [WeatherHeader] for the given [weather].\n  const WeatherHeader(this.weather, {super.key});\n\n  /// Returns a skeleton placeholder that mirrors the layout of [WeatherHeader].\n  static Widget skeleton(BuildContext context) {\n    return Skeletonizer.zone(\n      child: Center(\n        child: Column(\n          spacing: 12,\n          children: [\n            Row(\n              mainAxisSize: .min,\n              spacing: 4,\n              children: [\n                const Bone.icon(size: 32),\n                Bone.text(\n                  words: 1,\n                  style: context.texts.titleLarge,\n                ),\n              ],\n            ),\n            Bone.text(\n              width: 140,\n              style: context.texts.displayLarge,\n            ),\n            Row(\n              mainAxisSize: .min,\n              spacing: 12,\n              children: [\n                Bone.text(\n                  width: 60,\n                  style: context.texts.bodyLarge,\n                ),\n                Bone.text(\n                  width: 60,\n                  style: context.texts.bodyLarge,\n                ),\n              ],\n            ),\n            Wrap(\n              spacing: 12,\n              runSpacing: 8,\n              alignment: .center,\n              children: List.generate(\n                9,\n                (_) => Bone.text(\n                  width: 50,\n                  style: context.texts.bodySmall,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final e =\n        weather.data.humidity /\n        100 *\n        6.105 *\n        exp(\n          17.27 * weather.data.temperature / (weather.data.temperature + 237.3),\n        );\n    final feelsLike = weather.data.temperature + 0.33 * e - 0.7 * weather.data.wind.speed - 4.0;\n\n    return ResponsiveContainer(\n      child: Padding(\n        padding: const .symmetric(horizontal: 12),\n        child: Column(\n          spacing: 12,\n          children: [\n            Row(\n              mainAxisSize: .min,\n              spacing: 6,\n              children: [\n                Container(\n                  padding: const .all(8),\n                  decoration: BoxDecoration(\n                    color: context.colors.secondaryContainer.withValues(\n                      alpha: 0.3,\n                    ),\n                    borderRadius: .circular(12),\n                  ),\n                  child: Icon(\n                    WeatherIcons.getWeatherIcon(\n                      weather.data.weatherCode,\n                      true,\n                    ),\n                    size: 32,\n                    color: context.colors.secondary,\n                  ),\n                ),\n                Text(\n                  WeatherIcons.getWeatherContent(\n                    context,\n                    weather.data.weatherCode,\n                  ),\n                  style: context.texts.titleLarge!.copyWith(\n                    color: context.colors.secondary,\n                    fontWeight: .bold,\n                  ),\n                ),\n              ],\n            ),\n            Selector<SettingsUserInterfaceModel, bool>(\n              selector: (context, model) => model.useFahrenheit,\n              builder: (context, useFahrenheit, child) {\n                final value = weather.data.temperature;\n                final displayTemp = (useFahrenheit ? value.asFahrenheit : value).round();\n                final displayFeelsLike = (useFahrenheit ? feelsLike.asFahrenheit : feelsLike)\n                    .round();\n\n                return Column(\n                  spacing: 8,\n                  children: [\n                    Text(\n                      '$displayTemp°',\n                      style: context.texts.displayLarge!.copyWith(\n                        fontSize: 64,\n                        color: context.colors.onSurface,\n                        fontWeight: .w200,\n                        height: 1.0,\n                        letterSpacing: -2,\n                      ),\n                    ),\n                    Container(\n                      padding: const .symmetric(\n                        horizontal: 12,\n                        vertical: 6,\n                      ),\n                      decoration: BoxDecoration(\n                        color: context.colors.surfaceContainerHighest.withValues(alpha: 0.6),\n                        borderRadius: .circular(20),\n                      ),\n                      child: Text(\n                        '體感 $displayFeelsLike°'.i18n,\n                        style: context.texts.bodyLarge!.copyWith(\n                          color: context.colors.onSurfaceVariant,\n                          fontWeight: .w500,\n                        ),\n                      ),\n                    ),\n                  ],\n                );\n              },\n            ),\n            Wrap(\n              spacing: 10,\n              runSpacing: 8,\n              alignment: .center,\n              children: [\n                _buildInfoChip(\n                  context,\n                  Symbols.water_drop_rounded,\n                  '濕度'.i18n,\n                  '${weather.data.humidity >= 0 ? weather.data.humidity.round() : \"-\"}%',\n                  Colors.blue,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.wind_power_rounded,\n                  '風速'.i18n,\n                  weather.data.wind.speed >= 0 ? '${weather.data.wind.speed}m/s' : '-',\n                  Colors.teal,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.explore_rounded,\n                  '風向'.i18n,\n                  weather.data.wind.direction.isNotEmpty ? weather.data.wind.direction : '-',\n                  Colors.cyan,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.wind_power_rounded,\n                  '風級'.i18n,\n                  weather.data.wind.beaufort > 0 ? '${weather.data.wind.beaufort}級'.i18n : '-',\n                  Colors.teal,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.compress_rounded,\n                  '氣壓'.i18n,\n                  weather.data.pressure >= 0 ? '${weather.data.pressure.round()}hPa' : '-',\n                  Colors.orange,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.rainy_rounded,\n                  '降雨'.i18n,\n                  weather.data.rain >= 0 ? '${weather.data.rain}mm' : '-',\n                  Colors.indigo,\n                ),\n                _buildInfoChip(\n                  context,\n                  Symbols.visibility_rounded,\n                  '能見度'.i18n,\n                  weather.data.visibility >= 0 ? '${weather.data.visibility.round()}km' : '-',\n                  Colors.grey,\n                ),\n                if (weather.data.gust.speed > 0)\n                  _buildInfoChip(\n                    context,\n                    Symbols.air_rounded,\n                    '陣風'.i18n,\n                    '${weather.data.gust.speed}m/s',\n                    Colors.purple,\n                  ),\n                if (weather.data.gust.beaufort > 0)\n                  _buildInfoChip(\n                    context,\n                    Symbols.wind_power_rounded,\n                    '陣風級'.i18n,\n                    '${weather.data.gust.beaufort}級'.i18n,\n                    Colors.deepPurple,\n                  ),\n                if (weather.data.sunshine >= 0)\n                  _buildInfoChip(\n                    context,\n                    Symbols.wb_sunny_rounded,\n                    '日照'.i18n,\n                    '${weather.data.sunshine.toStringAsFixed(1)}h',\n                    Colors.amber,\n                  ),\n              ],\n            ),\n            Container(\n              margin: const .only(top: 4),\n              padding: const .symmetric(horizontal: 10, vertical: 6),\n              decoration: BoxDecoration(\n                color: context.colors.surfaceContainerHighest.withValues(\n                  alpha: 0.4,\n                ),\n                borderRadius: .circular(16),\n              ),\n              child: Row(\n                mainAxisSize: .min,\n                spacing: 6,\n                children: [\n                  Icon(\n                    Symbols.pin_drop_rounded,\n                    size: 14,\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                  Text(\n                    '${weather.station.name}氣象站'.i18n,\n                    style: context.texts.bodySmall!.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                  Container(\n                    width: 1,\n                    height: 12,\n                    margin: const .symmetric(horizontal: 4),\n                    color: context.colors.onSurfaceVariant.withValues(\n                      alpha: 0.3,\n                    ),\n                  ),\n                  Text(\n                    '${weather.station.distance.toStringAsFixed(1)}km',\n                    style: context.texts.bodySmall!.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                  Container(\n                    width: 1,\n                    height: 12,\n                    margin: const .symmetric(horizontal: 4),\n                    color: context.colors.onSurfaceVariant.withValues(\n                      alpha: 0.3,\n                    ),\n                  ),\n                  Icon(\n                    Symbols.schedule_rounded,\n                    size: 14,\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                  Text(\n                    weather.time.toLocaleTimeString(context).substring(0, 5),\n                    style: context.texts.bodySmall!.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildInfoChip(\n    BuildContext context,\n    IconData icon,\n    String label,\n    String text,\n    Color color,\n  ) {\n    return Container(\n      padding: const .symmetric(horizontal: 10, vertical: 6),\n      decoration: BoxDecoration(\n        color: color.withValues(alpha: 0.1),\n        borderRadius: .circular(12),\n        border: Border.all(color: color.withValues(alpha: 0.2), width: 1),\n      ),\n      child: Row(\n        mainAxisSize: .min,\n        spacing: 5,\n        children: [\n          Icon(icon, size: 14, color: color, weight: 600),\n          Column(\n            crossAxisAlignment: .start,\n            mainAxisSize: .min,\n            children: [\n              Text(\n                label,\n                style: context.texts.bodySmall!.copyWith(\n                  color: context.colors.onSurfaceVariant,\n                  fontSize: 9,\n                  height: 1.0,\n                ),\n              ),\n              Text(\n                text,\n                style: context.texts.bodySmall!.copyWith(\n                  color: context.colors.onSurface,\n                  fontWeight: .w600,\n                  fontSize: 11,\n                  height: 1.2,\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/_widgets/wind_card.dart",
    "content": "/// Wind and compass card displaying real-time wind data for the home screen.\nlibrary;\n\nimport 'dart:async';\nimport 'dart:math' as math;\n\nimport 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/core/compass.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_compass/flutter_compass.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// Compass accuracy threshold (degrees) for a warning indicator.\nconst double _kCompassAccuracyWarning = 25.0;\n\n/// Compass accuracy threshold (degrees) for a danger indicator.\nconst double _kCompassAccuracyDanger = 45.0;\n\n/// Maps Chinese cardinal/intercardinal direction names to bearing angles.\nconst _kWindDirections = {\n  '北': 0.0,\n  '北北東': 22.5,\n  '東北': 45.0,\n  '東北東': 67.5,\n  '東': 90.0,\n  '東南東': 112.5,\n  '東南': 135.0,\n  '南南東': 157.5,\n  '南': 180.0,\n  '南南西': 202.5,\n  '西南': 225.0,\n  '西南西': 247.5,\n  '西': 270.0,\n  '西北西': 292.5,\n  '西北': 315.0,\n  '北北西': 337.5,\n};\n\n/// Beaufort scale descriptions indexed by Beaufort number (0–12).\nconst _kBeaufortDescriptions = [\n  '無風',\n  '軟風',\n  '輕風',\n  '微風',\n  '和風',\n  '清風',\n  '強風',\n  '疾風',\n  '大風',\n  '烈風',\n  '狂風',\n  '暴風',\n  '颶風',\n];\n\n/// Shows a compact wind card with a live compass rose and magnetic accuracy\n/// indicator.\n///\n/// The compass rose rotates to reflect the device heading when a magnetometer\n/// is available. Tapping the accuracy indicator opens a diagnostic dialog.\nclass WindCard extends StatefulWidget {\n  /// The current weather observation used to display wind data.\n  final RealtimeWeather weather;\n\n  /// Creates a [WindCard] for the provided [weather].\n  const WindCard(this.weather, {super.key});\n\n  @override\n  State<WindCard> createState() => _WindCardState();\n}\n\nclass _WindCardState extends State<WindCard> with WidgetsBindingObserver, RouteAware {\n  StreamSubscription<CompassEvent>? _compassSubscription;\n  double _deviceHeading = 0.0;\n  double _compassAccuracy = 0.0;\n  bool _hasCompass = false;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _initCompass();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    final route = ModalRoute.of(context);\n    TalkerManager.instance.debug(\n      'WindCard.didChangeDependencies: route=$route',\n    );\n    if (route != null) {\n      routeObserver.subscribe(this, route);\n    }\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    TalkerManager.instance.debug('WindCard.didChangeAppLifecycleState: $state');\n    if (state == .resumed) {\n      _initCompass();\n    } else if (state == .paused) {\n      _compassSubscription?.cancel();\n      _compassSubscription = null;\n    }\n  }\n\n  @override\n  void didPopNext() {\n    TalkerManager.instance.debug('WindCard.didPopNext called');\n    _initCompass();\n  }\n\n  @override\n  void didPush() {\n    TalkerManager.instance.debug(\n      'WindCard.didPush called, hasSubscription=${_compassSubscription != null}',\n    );\n    if (_compassSubscription == null) {\n      _initCompass();\n    }\n  }\n\n  @override\n  void didPushNext() {\n    TalkerManager.instance.debug('WindCard.didPushNext called');\n    _compassSubscription?.cancel();\n    _compassSubscription = null;\n  }\n\n  void _initCompass() {\n    TalkerManager.instance.debug(\n      'WindCard._initCompass called, mounted=$mounted, '\n      'hasSubscription=${_compassSubscription != null}',\n    );\n\n    if (_compassSubscription != null) {\n      TalkerManager.instance.debug(\n        'WindCard._initCompass: already has subscription, skipping',\n      );\n      return;\n    }\n\n    final compass = CompassService.instance;\n    if (!compass.hasCompass) {\n      TalkerManager.instance.debug(\n        'WindCard._initCompass: compass not available',\n      );\n      return;\n    }\n\n    _deviceHeading = compass.lastHeading;\n    _hasCompass = true;\n    TalkerManager.instance.debug(\n      'WindCard._initCompass: using lastHeading=${compass.lastHeading}',\n    );\n\n    _compassSubscription = compass.events?.listen((event) {\n      if (event.heading != null && mounted) {\n        setState(() {\n          _deviceHeading = event.heading!;\n          if (event.accuracy != null) {\n            _compassAccuracy = event.accuracy!;\n          }\n        });\n      }\n    });\n    TalkerManager.instance.debug('WindCard._initCompass: subscription created');\n  }\n\n  double _getWindDirectionAngle(String direction) => _kWindDirections[direction.trim()] ?? 0.0;\n\n  String _getBeaufortDescription(int beaufort) =>\n      ((beaufort >= 0 && beaufort < _kBeaufortDescriptions.length)\n              ? _kBeaufortDescriptions[beaufort]\n              : '未知')\n          .i18n;\n\n  bool get _compassHasDanger => _compassAccuracy < 0 || _compassAccuracy >= _kCompassAccuracyDanger;\n\n  bool get _compassHasWarning =>\n      _compassAccuracy >= _kCompassAccuracyWarning && _compassAccuracy < _kCompassAccuracyDanger;\n\n  Color get _compassStatusColor {\n    if (_compassHasDanger) return Colors.red;\n    if (_compassHasWarning) return Colors.orange;\n    return Colors.green;\n  }\n\n  Widget _buildCompass(\n    BuildContext context,\n    double windAngle,\n    bool hasValidDirection,\n  ) {\n    final deviceRotation = -_deviceHeading * math.pi / 180;\n\n    return Stack(\n      alignment: .center,\n      children: [\n        Container(\n          decoration: BoxDecoration(\n            shape: .circle,\n            color: context.colors.surfaceContainerHighest.withValues(\n              alpha: 0.5,\n            ),\n            border: Border.all(\n              color: context.colors.outline.withValues(alpha: 0.2),\n              width: 2,\n            ),\n          ),\n        ),\n        Transform.rotate(\n          angle: deviceRotation,\n          child: Stack(\n            alignment: .center,\n            children: [\n              ...['N', 'E', 'S', 'W'].asMap().entries.map((entry) {\n                final index = entry.key;\n                final label = entry.value;\n                final angle = index * 90.0;\n                final isNorth = label == 'N';\n\n                return Transform.rotate(\n                  angle: angle * math.pi / 180,\n                  child: Align(\n                    alignment: const Alignment(0, -0.78),\n                    child: Transform.rotate(\n                      angle: -angle * math.pi / 180 - deviceRotation,\n                      child: Text(\n                        label,\n                        style: context.texts.labelMedium?.copyWith(\n                          color: isNorth ? Colors.red : context.colors.onSurfaceVariant,\n                          fontWeight: isNorth ? .bold : .w500,\n                          fontSize: isNorth ? 14 : 12,\n                        ),\n                      ),\n                    ),\n                  ),\n                );\n              }),\n              CustomPaint(\n                size: const Size.square(100),\n                painter: _CompassTicksPainter(\n                  color: context.colors.outline.withValues(alpha: 0.3),\n                ),\n              ),\n              if (hasValidDirection)\n                Transform.rotate(\n                  angle: windAngle * math.pi / 180,\n                  child: CustomPaint(\n                    size: const Size.square(80),\n                    painter: _WindArrowPainter(color: Colors.teal),\n                  ),\n                ),\n            ],\n          ),\n        ),\n        Container(\n          width: 12,\n          height: 12,\n          decoration: BoxDecoration(\n            shape: .circle,\n            color: hasValidDirection ? Colors.teal : context.colors.outline,\n            boxShadow: [\n              BoxShadow(\n                color: (hasValidDirection ? Colors.teal : context.colors.outline).withValues(\n                  alpha: 0.3,\n                ),\n                blurRadius: 4,\n                spreadRadius: 1,\n              ),\n            ],\n          ),\n        ),\n        if (!hasValidDirection)\n          Positioned(\n            bottom: 8,\n            child: Text(\n              '無資料'.i18n,\n              style: context.texts.labelSmall?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget _buildCompactWindInfo(\n    BuildContext context,\n    RealtimeWeatherWind wind,\n    RealtimeWeatherGust gust,\n    bool hasValidDirection,\n  ) {\n    return Column(\n      crossAxisAlignment: .start,\n      mainAxisSize: .min,\n      children: [\n        Row(\n          children: [\n            Icon(Symbols.air_rounded, size: 16, color: Colors.teal),\n            const SizedBox(width: 6),\n            Text(\n              hasValidDirection ? wind.direction.trim() : '-',\n              style: context.texts.titleMedium?.copyWith(\n                fontWeight: .bold,\n              ),\n            ),\n            const SizedBox(width: 8),\n            Text(\n              wind.speed >= 0 ? '${wind.speed} m/s' : '-',\n              style: context.texts.bodyMedium?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n          ],\n        ),\n        const SizedBox(height: 4),\n        Row(\n          children: [\n            Text(\n              wind.beaufort >= 0\n                  ? '{wind}級 {Desc}'.i18n.args({\n                      'wind': wind.beaufort,\n                      'Desc': _getBeaufortDescription(wind.beaufort),\n                    })\n                  : '-',\n              style: context.texts.bodySmall?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n            if (gust.speed > 0) ...[\n              Text(\n                ' · ',\n                style: context.texts.bodySmall?.copyWith(\n                  color: context.colors.outline,\n                ),\n              ),\n              Text(\n                '陣風 {speed} m/s'.i18n.args({'speed': gust.speed}),\n                style: context.texts.bodySmall?.copyWith(\n                  color: Colors.purple,\n                ),\n              ),\n            ],\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildMagneticInfo(BuildContext context) {\n    final hasDanger = _compassHasDanger;\n    final hasWarning = _compassHasWarning;\n    final statusText = _compassAccuracy < 0 ? '–' : '±${_compassAccuracy.round()}°';\n    final statusColor = _compassStatusColor;\n\n    return GestureDetector(\n      onTap: () => _showMagneticFieldInfo(context),\n      child: Container(\n        padding: const .symmetric(horizontal: 8, vertical: 6),\n        decoration: BoxDecoration(\n          color: statusColor.withValues(alpha: 0.1),\n          borderRadius: .circular(8),\n          border: (hasWarning || hasDanger)\n              ? Border.all(color: statusColor.withValues(alpha: 0.5))\n              : null,\n        ),\n        child: Column(\n          mainAxisSize: .min,\n          children: [\n            Icon(\n              hasDanger || hasWarning ? Symbols.warning_rounded : Symbols.explore_rounded,\n              size: 16,\n              color: statusColor,\n            ),\n            const SizedBox(height: 2),\n            Text(\n              '${_deviceHeading.round()}°',\n              style: context.texts.labelSmall?.copyWith(\n                color: statusColor,\n                fontWeight: .bold,\n              ),\n            ),\n            Text(\n              statusText,\n              style: context.texts.labelSmall?.copyWith(\n                color: statusColor.withValues(alpha: 0.8),\n                fontSize: 9,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _showMagneticFieldInfo(BuildContext context) {\n    final hasDanger = _compassHasDanger;\n    final hasWarning = _compassHasWarning;\n    final valueText = _compassAccuracy < 0\n        ? '無法測量'.i18n\n        : '±${_compassAccuracy.toStringAsFixed(1)}°';\n\n    showDialog(\n      context: context,\n      builder: (context) => AlertDialog(\n        icon: Icon(\n          hasDanger\n              ? Symbols.error_rounded\n              : hasWarning\n              ? Symbols.warning_rounded\n              : Symbols.check_circle_rounded,\n          color: hasDanger\n              ? Colors.red\n              : hasWarning\n              ? Colors.orange\n              : Colors.green,\n          size: 48,\n        ),\n        title: Text(\n          hasDanger\n              ? '指北針不可靠'.i18n\n              : hasWarning\n              ? '指北針準確度下降'.i18n\n              : '指北針正常'.i18n,\n        ),\n        content: Column(\n          mainAxisSize: .min,\n          crossAxisAlignment: .start,\n          children: [\n            Text(\n              '方向精確度'.i18n,\n              style: context.texts.labelMedium?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n            const SizedBox(height: 4),\n            Text(\n              valueText,\n              style: context.texts.headlineSmall?.copyWith(\n                fontWeight: .bold,\n                color: hasDanger\n                    ? Colors.red\n                    : hasWarning\n                    ? Colors.orange\n                    : Colors.green,\n              ),\n            ),\n            const SizedBox(height: 12),\n            Text(\n              '正常範圍：±0-15°'.i18n,\n              style: context.texts.bodySmall?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n            if (hasWarning || hasDanger) ...[\n              const SizedBox(height: 12),\n              Container(\n                padding: const .all(12),\n                decoration: BoxDecoration(\n                  color: (hasDanger ? Colors.red : Colors.orange).withValues(\n                    alpha: 0.1,\n                  ),\n                  borderRadius: .circular(8),\n                ),\n                child: Text(\n                  hasDanger\n                      ? '附近有強磁場干擾，指北針方向可能完全不準確。請遠離磁鐵、電子裝置或金屬物品。'.i18n\n                      : '附近可能有磁場干擾，指北針方向可能有偏差。'.i18n,\n                  style: context.texts.bodySmall,\n                ),\n              ),\n            ],\n          ],\n        ),\n        actions: [\n          TextButton(\n            onPressed: () => context.navigator.pop(),\n            child: Text('確定'.i18n),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final wind = widget.weather.data.wind;\n    final gust = widget.weather.data.gust;\n    final hasValidDirection = wind.direction.isNotEmpty && wind.direction != '-';\n    final windAngle = hasValidDirection ? _getWindDirectionAngle(wind.direction) : 0.0;\n\n    return ResponsiveContainer(\n      child: Container(\n        margin: const .symmetric(horizontal: 16, vertical: 8),\n        decoration: BoxDecoration(\n          color: context.colors.surfaceContainerLow,\n          borderRadius: .circular(16),\n          border: Border.all(\n            color: context.colors.outlineVariant.withValues(alpha: 0.5),\n          ),\n        ),\n        child: Padding(\n          padding: const .all(12),\n          child: Row(\n            children: [\n              SizedBox(\n                width: 80,\n                height: 80,\n                child: _buildCompass(context, windAngle, hasValidDirection),\n              ),\n              const SizedBox(width: 12),\n              Expanded(\n                child: _buildCompactWindInfo(\n                  context,\n                  wind,\n                  gust,\n                  hasValidDirection,\n                ),\n              ),\n              if (_hasCompass) _buildMagneticInfo(context),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    routeObserver.unsubscribe(this);\n    WidgetsBinding.instance.removeObserver(this);\n    _compassSubscription?.cancel();\n    super.dispose();\n  }\n}\n\n/// Draws the compass tick marks around the perimeter of the rose.\nclass _CompassTicksPainter extends CustomPainter {\n  /// The colour used to draw the tick marks.\n  final Color color;\n\n  /// Creates a [_CompassTicksPainter] with the given [color].\n  const _CompassTicksPainter({required this.color});\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final center = Offset(size.width / 2, size.height / 2);\n    final radius = size.width / 2;\n    final paint = Paint()\n      ..color = color\n      ..strokeWidth = 1;\n\n    for (int i = 0; i < 36; i++) {\n      final angle = i * 10 * math.pi / 180;\n      final isMainTick = i % 9 == 0;\n      final startRadius = radius - (isMainTick ? 12 : 6);\n      final endRadius = radius - 2;\n\n      final start = Offset(\n        center.dx + startRadius * math.sin(angle),\n        center.dy - startRadius * math.cos(angle),\n      );\n      final end = Offset(\n        center.dx + endRadius * math.sin(angle),\n        center.dy - endRadius * math.cos(angle),\n      );\n\n      paint.strokeWidth = isMainTick ? 2 : 1;\n      canvas.drawLine(start, end, paint);\n    }\n  }\n\n  @override\n  bool shouldRepaint(covariant _CompassTicksPainter oldDelegate) => color != oldDelegate.color;\n}\n\n/// Draws a directional arrow indicating the current wind direction.\nclass _WindArrowPainter extends CustomPainter {\n  /// The colour used to fill the arrow.\n  final Color color;\n\n  /// Creates a [_WindArrowPainter] with the given [color].\n  const _WindArrowPainter({required this.color});\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final center = Offset(size.width / 2, size.height / 2);\n    final radius = size.width / 2;\n\n    final shadowPaint = Paint()\n      ..color = Colors.black.withValues(alpha: 0.25)\n      ..style = PaintingStyle.fill\n      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);\n\n    final shaftPath = Path()\n      ..moveTo(center.dx - 1, center.dy - radius * 0.25)\n      ..lineTo(center.dx - 1, center.dy + radius * 0.45)\n      ..lineTo(center.dx + 1, center.dy + radius * 0.45)\n      ..lineTo(center.dx + 1, center.dy - radius * 0.25)\n      ..close();\n\n    canvas.drawPath(shaftPath.shift(const Offset(0.5, 0.5)), shadowPaint);\n    canvas.drawPath(shaftPath, Paint()..color = color.withValues(alpha: 0.6));\n\n    final tailStartY = center.dy + radius * 0.45;\n    final tailEndY = center.dy + radius * 0.72;\n\n    final leftTailPath = Path()\n      ..moveTo(center.dx, tailStartY)\n      ..lineTo(center.dx - 10, tailEndY)\n      ..lineTo(center.dx, tailEndY - radius * 0.06)\n      ..close();\n\n    final rightTailPath = Path()\n      ..moveTo(center.dx, tailStartY)\n      ..lineTo(center.dx + 10, tailEndY)\n      ..lineTo(center.dx, tailEndY - radius * 0.06)\n      ..close();\n\n    canvas.drawPath(leftTailPath.shift(const Offset(0.5, 0.5)), shadowPaint);\n    canvas.drawPath(rightTailPath.shift(const Offset(0.5, 0.5)), shadowPaint);\n    canvas.drawPath(leftTailPath, Paint()..color = color);\n    canvas.drawPath(rightTailPath, Paint()..color = color);\n\n    final arrowTip = Offset(center.dx, center.dy - radius * 0.42);\n    final arrowPath = Path()\n      ..moveTo(arrowTip.dx, arrowTip.dy)\n      ..lineTo(center.dx - 5, center.dy - radius * 0.22)\n      ..lineTo(center.dx, center.dy - radius * 0.25)\n      ..lineTo(center.dx + 5, center.dy - radius * 0.22)\n      ..close();\n\n    canvas.drawPath(arrowPath.shift(const Offset(0.5, 0.5)), shadowPaint);\n    canvas.drawPath(arrowPath, Paint()..color = color);\n  }\n\n  @override\n  bool shouldRepaint(covariant _WindArrowPainter oldDelegate) => color != oldDelegate.color;\n}\n"
  },
  {
    "path": "lib/app/home/layout.dart",
    "content": "/// Home screen layout providing the [HomeLocationModel] to descendant widgets.\nlibrary;\n\nimport 'package:dpip/app/home/_models/home_location.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// Wraps [child] in a [Scaffold] and provides a [HomeLocationModel] scoped to\n/// the home feature.\nclass HomeLayout extends StatelessWidget {\n  /// The widget subtree that receives [HomeLocationModel].\n  final Widget child;\n\n  /// Creates a [HomeLayout] with the given [child].\n  const HomeLayout({super.key, required this.child});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: ChangeNotifierProvider<HomeLocationModel>(\n        create: (context) => HomeLocationModel(),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/home/page.dart",
    "content": "/// Main home page presenting weather, EEW alerts, and event history.\nlibrary;\n\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/app/home/_models/home_location.dart';\nimport 'package:dpip/app/home/_widgets/blurred_button.dart';\nimport 'package:dpip/app/home/_widgets/date_timeline_item.dart';\nimport 'package:dpip/app/home/_widgets/eew_card.dart';\nimport 'package:dpip/app/home/_widgets/forecast_card.dart';\nimport 'package:dpip/app/home/_widgets/hero_weather.dart';\nimport 'package:dpip/app/home/_widgets/history_timeline_item.dart';\nimport 'package:dpip/app/home/_widgets/location_button.dart';\nimport 'package:dpip/app/home/_widgets/location_not_set_card.dart';\nimport 'package:dpip/app/home/_widgets/location_out_of_service.dart';\nimport 'package:dpip/app/home/_widgets/mode_toggle_button.dart';\nimport 'package:dpip/app/home/_widgets/radar_card.dart';\nimport 'package:dpip/app/home/_widgets/thunderstorm_card.dart';\nimport 'package:dpip/app/home/_widgets/wind_card.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/map.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/shader_selector.dart';\nimport 'package:dpip/utils/wallpaper_selector.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\nimport 'package:simple_icons/simple_icons.dart';\nimport 'package:timezone/timezone.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// The main home screen widget.\n///\n/// Composes a full-screen shader background, a hero weather section, overlay\n/// buttons, and a draggable bottom sheet containing weather cards and the\n/// event timeline.\nclass HomePage extends StatefulWidget {\n  /// Creates the [HomePage].\n  const HomePage({super.key});\n\n  @override\n  State<HomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State<HomePage> with WidgetsBindingObserver {\n  DraggableScrollableController _sheetController = .new();\n\n  Key _mapKey = UniqueKey();\n  bool _isLoading = false;\n  bool _isOutOfService = false;\n  bool _wasVisible = true;\n\n  final _blurNotifier = ValueNotifier<double>(0.0);\n  final _isFullyExpandedNotifier = ValueNotifier<bool>(false);\n\n  final _firstCardKey = GlobalKey();\n  double? _measuredFirstCardHeight;\n  Key _sheetKey = UniqueKey();\n\n  static const double _defaultFirstCardHeight = 280.0;\n\n  RealtimeWeather? _weather;\n  Map<String, dynamic>? _forecast;\n  List<History>? _history;\n  List<History>? _realtimeRegion;\n  HomeMode _currentMode = HomeMode.localActive;\n\n  String? _lastRefreshCode;\n  bool _isFirstRefresh = true;\n\n  HomeLocationModel? _homeLocationModel;\n\n  History? get _thunderstorm => _realtimeRegion\n      ?.where((e) => e.type == .thunderstorm)\n      .sorted((a, b) => b.time.send.compareTo(a.time.send))\n      .firstOrNull;\n\n  void _measureFirstCard() {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      final cardContext = _firstCardKey.currentContext;\n      if (cardContext != null) {\n        final box = cardContext.findRenderObject() as RenderBox?;\n        if (box != null && box.hasSize) {\n          final height = box.size.height + 28;\n          final isFirstMeasure = _measuredFirstCardHeight == null;\n          if (isFirstMeasure || ((_measuredFirstCardHeight! - height).abs() > 1)) {\n            _sheetController.removeListener(_onSheetChanged);\n            _sheetController.dispose();\n\n            setState(() {\n              _measuredFirstCardHeight = height;\n              _sheetKey = UniqueKey();\n              _sheetController = DraggableScrollableController();\n            });\n\n            _sheetController.addListener(_onSheetChanged);\n\n            if (isFirstMeasure && mounted) {\n              WidgetsBinding.instance.addPostFrameCallback((_) {\n                final screenHeight = context.dimension.height;\n                final targetSize = (height / screenHeight).clamp(0.25, 0.6);\n                _sheetController.animateTo(\n                  targetSize,\n                  duration: const Duration(milliseconds: 350),\n                  curve: Curves.easeOutCubic,\n                );\n              });\n            }\n          }\n        }\n      }\n    });\n  }\n\n  double get _firstCardHeight => _measuredFirstCardHeight ?? _defaultFirstCardHeight;\n\n  void _onSheetChanged() {\n    final size = _sheetController.size;\n    final screenHeight = context.dimension.height;\n    final baseSize = (_firstCardHeight / screenHeight).clamp(0.25, 0.6);\n    final progress = ((size - baseSize) / (1.0 - baseSize)).clamp(0.0, 1.0);\n    final newBlur = (progress * 15.0 / 5.0).round() * 5.0;\n    if ((_blurNotifier.value - newBlur).abs() >= 4.9) {\n      _blurNotifier.value = newBlur;\n    }\n    final isFullyExpanded = size >= 0.99;\n    if (_isFullyExpandedNotifier.value != isFullyExpanded) {\n      _isFullyExpandedNotifier.value = isFullyExpanded;\n    }\n  }\n\n  void _checkVersion() {\n    Preference.version ??= Global.packageInfo.version;\n    if (Global.packageInfo.version == Preference.version) return;\n\n    Preference.version = Global.packageInfo.version;\n    context.scaffoldMessenger.showSnackBar(\n      SnackBar(\n        content: Text(\n          '已更新至 {version}'.i18n.args({\n            'version': 'v${Global.packageInfo.version}',\n          }),\n        ),\n        action: SnackBarAction(\n          label: '更新日誌'.i18n,\n          onPressed: () => ChangelogRoute().push(context),\n        ),\n        duration: kPersistSnackBar,\n      ),\n    );\n  }\n\n  String? get _effectiveLocationCode =>\n      _homeLocationModel?.temporaryCode ?? GlobalProviders.location.code;\n\n  Future<void> _refresh() async {\n    if (_isLoading) return;\n\n    if (_homeLocationModel?.temporaryCode == null) {\n      await _reloadLocationData();\n    }\n\n    final code = _effectiveLocationCode;\n\n    final isOutOfService = _checkIfOutOfService(code);\n\n    if (isOutOfService && !_currentMode.isNational) {\n      _currentMode = _currentMode.isActive ? .nationalActive : .nationalHistory;\n    }\n\n    setState(() {\n      _isLoading = true;\n      _isOutOfService = isOutOfService;\n      if (!_isFirstRefresh && _lastRefreshCode != code) {\n        _mapKey = Key('${DateTime.now().millisecondsSinceEpoch}');\n        _weather = null;\n        _forecast = null;\n      }\n      _isFirstRefresh = false;\n    });\n\n    final futures = <Future>[\n      _fetchWeather(code),\n      _fetchRealtimeRegion(code),\n      _fetchHistory(code, isOutOfService),\n    ];\n\n    await Future.wait(futures);\n\n    if (mounted) {\n      setState(() => _isLoading = false);\n      _lastRefreshCode = code;\n    }\n  }\n\n  Future<void> _reloadLocationData() async {\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    } else {\n      await Preference.reload();\n      final code = Preference.locationCode;\n      if (code != null) {\n        final location = Global.location[code];\n        if (location != null) {\n          Preference.locationLatitude = location.lat;\n          Preference.locationLongitude = location.lng;\n        }\n      }\n      GlobalProviders.location.refresh();\n    }\n  }\n\n  bool _checkIfOutOfService(String? code) {\n    if (code == null) return true;\n\n    if (_homeLocationModel?.temporaryCode != null) return false;\n\n    final auto = GlobalProviders.location.auto;\n    final location = Global.location[code];\n\n    return auto && location == null;\n  }\n\n  Future<void> _fetchWeather(String? code) async {\n    if (code == null) {\n      if (mounted)\n        setState(() {\n          _weather = null;\n          _forecast = null;\n        });\n      return;\n    }\n\n    try {\n      LatLng? coords;\n\n      final temporaryCode = _homeLocationModel?.temporaryCode;\n      if (temporaryCode != null) {\n        final location = Global.location[temporaryCode];\n        if (location != null) {\n          coords = LatLng(location.lat, location.lng);\n        }\n      } else if (Preference.locationLatitude != null && Preference.locationLongitude != null) {\n        coords = LatLng(\n          Preference.locationLatitude!,\n          Preference.locationLongitude!,\n        );\n      } else {\n        coords = GlobalProviders.location.coordinates;\n      }\n\n      if (coords != null) {\n        final weather = await ExpTech().getWeatherRealtimeByCoords(\n          coords.latitude,\n          coords.longitude,\n        );\n        if (mounted) setState(() => _weather = weather);\n      } else {\n        if (mounted) setState(() => _weather = null);\n      }\n\n      final forecast = await ExpTech().getWeatherForecast(code);\n      if (mounted) setState(() => _forecast = forecast);\n    } catch (e, s) {\n      if (!mounted) return;\n      TalkerManager.instance.error('_HomePageState._fetchWeather', e, s);\n      context.scaffoldMessenger.showSnackBar(\n        SnackBar(content: Text('取得天氣異常'.i18n)),\n      );\n    }\n  }\n\n  Future<void> _fetchRealtimeRegion(String? code) async {\n    if (code == null) {\n      if (mounted) setState(() => _realtimeRegion = null);\n      return;\n    }\n\n    try {\n      final realtime = await ExpTech().getRealtimeRegion(code);\n      if (mounted) setState(() => _realtimeRegion = realtime);\n    } catch (e, s) {\n      if (!mounted) return;\n      TalkerManager.instance.error(\n        '_HomePageState._fetchRealtimeRegion',\n        e,\n        s,\n      );\n      if (mounted) setState(() => _realtimeRegion = null);\n    }\n  }\n\n  Future<void> _fetchHistory(String? code, bool isOutOfService) async {\n    try {\n      final shouldUseNational = _currentMode.isNational || isOutOfService || code == null;\n      final List<History> history;\n\n      if (shouldUseNational) {\n        history = _currentMode.isActive\n            ? await ExpTech().getRealtime()\n            : await ExpTech().getHistory();\n      } else {\n        history = _currentMode.isActive\n            ? await ExpTech().getRealtimeRegion(code)\n            : await ExpTech().getHistoryRegion(code);\n      }\n\n      if (mounted) setState(() => _history = history);\n    } catch (e, s) {\n      if (!mounted) return;\n      TalkerManager.instance.error('_HomePageState._fetchHistory', e, s);\n      context.scaffoldMessenger.showSnackBar(\n        SnackBar(content: Text('取得歷史資訊異常'.i18n)),\n      );\n    }\n  }\n\n  void _onModeChanged(HomeMode mode) {\n    setState(() => _currentMode = mode);\n    _refresh();\n  }\n\n  Widget _buildDraggableSheet(List<HomeDisplaySection> homeSections) {\n    final screenHeight = context.dimension.height;\n    final baseSnapSize = (_firstCardHeight / screenHeight).clamp(0.25, 0.6);\n    final handleHeight = 28.0 / screenHeight;\n    final minSize = handleHeight.clamp(0.03, 0.05);\n    final initialSize = _measuredFirstCardHeight == null ? minSize : baseSnapSize;\n    final snapSizes = [minSize, baseSnapSize, 1.0];\n\n    return DraggableScrollableSheet(\n      key: _sheetKey,\n      controller: _sheetController,\n      initialChildSize: initialSize,\n      minChildSize: minSize,\n      maxChildSize: 1.0,\n      snap: true,\n      snapSizes: snapSizes,\n      builder: (context, scrollController) {\n        return SingleChildScrollView(\n          controller: scrollController,\n          child: Column(\n            children: [\n              RepaintBoundary(\n                child: ValueListenableBuilder<bool>(\n                  valueListenable: _isFullyExpandedNotifier,\n                  builder: (context, isFullyExpanded, child) {\n                    return AnimatedOpacity(\n                      opacity: isFullyExpanded ? 0.0 : 1.0,\n                      duration: const Duration(milliseconds: 150),\n                      child: Center(\n                        child: Container(\n                          margin: const .symmetric(vertical: 12),\n                          width: 32,\n                          height: 4,\n                          decoration: BoxDecoration(\n                            color: context.colors.onSurfaceVariant.withValues(\n                              alpha: 0.4,\n                            ),\n                            borderRadius: .circular(2),\n                          ),\n                        ),\n                      ),\n                    );\n                  },\n                ),\n              ),\n              RepaintBoundary(\n                child: _buildContentSection(homeSections),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildHeroSection() {\n    final code = _effectiveLocationCode;\n    final screenHeight = context.dimension.height;\n\n    if (code == null) {\n      return SizedBox(\n        height: screenHeight * 0.5,\n        child: const Center(\n          child: Padding(\n            padding: .all(32),\n            child: LocationNotSetCard(),\n          ),\n        ),\n      );\n    }\n\n    if (_isOutOfService) {\n      return SizedBox(\n        height: screenHeight * 0.5,\n        child: const Center(\n          child: Padding(\n            padding: .all(32),\n            child: LocationOutOfServiceCard(),\n          ),\n        ),\n      );\n    }\n\n    return HeroWeather(\n      weather: _weather,\n      isLoading: _isLoading,\n    );\n  }\n\n  Widget _buildContentSection(List<HomeDisplaySection> homeSections) {\n    final List<Widget> allCards = [];\n    final List<Widget> firstCardChildren = [];\n\n    final stationInfo = _buildStationInfo();\n    firstCardChildren.add(stationInfo);\n\n    Widget? firstSectionWidget;\n    int? firstSectionIndex;\n\n    if (!_isLoading) {\n      final realtimeWidgets = _buildRealtimeInfo();\n      allCards.addAll(realtimeWidgets);\n    }\n    for (var i = 0; i < homeSections.length; i++) {\n      final section = homeSections[i];\n      switch (section) {\n        case HomeDisplaySection.radar:\n          firstSectionWidget = _buildRadarMap();\n          firstSectionIndex = i;\n        case HomeDisplaySection.forecast:\n          firstSectionWidget = _buildForecast();\n          firstSectionIndex = i;\n        case HomeDisplaySection.wind:\n          if (!_isLoading && _weather != null) {\n            firstSectionWidget = _buildWindCard();\n            firstSectionIndex = i;\n          }\n      }\n      if (firstSectionWidget != null) break;\n    }\n\n    if (firstSectionWidget != null) {\n      firstCardChildren.add(firstSectionWidget);\n    }\n\n    allCards.add(\n      KeyedSubtree(\n        key: _firstCardKey,\n        child: Column(\n          mainAxisSize: .min,\n          children: firstCardChildren,\n        ),\n      ),\n    );\n    WidgetsBinding.instance.addPostFrameCallback((_) => _measureFirstCard());\n    for (var i = 0; i < homeSections.length; i++) {\n      if (i == firstSectionIndex) continue;\n      final section = homeSections[i];\n      switch (section) {\n        case HomeDisplaySection.radar:\n          allCards.add(_buildRadarMap());\n        case HomeDisplaySection.forecast:\n          allCards.add(_buildForecast());\n        case HomeDisplaySection.wind:\n          if (_weather != null) {\n            allCards.add(_buildWindCard());\n          }\n      }\n    }\n\n    allCards.add(_buildHistoryTimeline());\n    allCards.add(_buildCommunityCards());\n\n    return Column(\n      children: [\n        ...allCards,\n        SizedBox(height: context.padding.bottom + 16),\n      ],\n    );\n  }\n\n  Widget _buildCommunityCards() {\n    final options = [\n      (\n        icon: SimpleIcons.discord,\n        color: const Color(0xFF5865F2),\n        url: 'https://exptech.com.tw/dc',\n      ),\n      (\n        icon: SimpleIcons.threads,\n        color: context.theme.brightness == .dark ? Colors.white : Colors.black,\n        url: 'https://www.threads.net/@dpip.tw',\n      ),\n      (\n        icon: SimpleIcons.youtube,\n        color: const Color(0xFFFF0000),\n        url: 'https://www.youtube.com/@exptechtw/live',\n      ),\n      (\n        icon: Symbols.favorite_rounded,\n        color: context.colors.primary,\n        url: SettingsDonateRoute().location,\n      ),\n    ];\n\n    return Padding(\n      padding: const .symmetric(horizontal: 16, vertical: 8),\n      child: Row(\n        mainAxisAlignment: .center,\n        children: options.map((item) {\n          return Padding(\n            padding: const .symmetric(horizontal: 4),\n            child: Material(\n              color: Colors.transparent,\n              child: InkWell(\n                onTap: () {\n                  if (item.url.startsWith('/')) {\n                    context.push(item.url);\n                  } else {\n                    launchUrl(Uri.parse(item.url));\n                  }\n                },\n                borderRadius: .circular(20),\n                child: Ink(\n                  padding: const .all(8),\n                  decoration: BoxDecoration(\n                    color: context.colors.surfaceContainerLow.withValues(\n                      alpha: 0.6,\n                    ),\n                    shape: .circle,\n                  ),\n                  child: Icon(item.icon, size: 24, color: item.color),\n                ),\n              ),\n            ),\n          );\n        }).toList(),\n      ),\n    );\n  }\n\n  Widget _buildStationInfo() {\n    final weather = _weather;\n    final hasData = weather != null;\n\n    String timeStr = '--:--';\n    if (hasData) {\n      final dt = DateTime.fromMillisecondsSinceEpoch(weather.time);\n      final hour = dt.hour;\n      final hour12 = hour == 0 ? 12 : (hour > 12 ? hour - 12 : hour);\n      final period = hour < 12 ? '上午'.i18n : '下午'.i18n;\n      timeStr =\n          '$period ${hour12.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';\n    }\n\n    String stationLabel = '--';\n    if (hasData) {\n      stationLabel = weather.station.name;\n      if (weather.station.distance >= 0) {\n        stationLabel += '・${weather.station.distance.toStringAsFixed(1)}km';\n      }\n    }\n\n    return ResponsiveContainer(\n      child: Padding(\n        padding: const .fromLTRB(16, 0, 16, 8),\n        child: Container(\n          padding: const .all(10),\n          decoration: BoxDecoration(\n            color: context.colors.surfaceContainer,\n            borderRadius: .circular(12),\n          ),\n          child: Column(\n            children: [\n              Row(\n                children: [\n                  Icon(\n                    Symbols.pin_drop_rounded,\n                    size: 15,\n                    color: context.colors.primary,\n                  ),\n                  const SizedBox(width: 4),\n                  Expanded(\n                    child: Text(\n                      stationLabel,\n                      style: context.texts.labelSmall?.copyWith(\n                        fontSize: 12,\n                        fontWeight: .w600,\n                        color: context.colors.onSurface,\n                      ),\n                      overflow: .ellipsis,\n                    ),\n                  ),\n                  Text(\n                    timeStr,\n                    style: context.texts.labelSmall?.copyWith(\n                      fontSize: 12,\n                      fontWeight: .w600,\n                      color: context.colors.onSurface,\n                    ),\n                  ),\n                ],\n              ),\n              const SizedBox(height: 8),\n              Wrap(\n                spacing: 6,\n                runSpacing: 6,\n                children: [\n                  _StationChip(\n                    label: '濕度'.i18n,\n                    value: hasData && weather.data.humidity >= 0\n                        ? '${weather.data.humidity.round().toString().padLeft(3)}%'\n                        : '---',\n                    color: Colors.cyan,\n                  ),\n                  _StationChip(\n                    label: '氣壓'.i18n,\n                    value: hasData && weather.data.pressure >= 0\n                        ? '${weather.data.pressure.round().toString().padLeft(4)}hPa'\n                        : '----',\n                    color: Colors.purple,\n                  ),\n                  _StationChip(\n                    label: '降雨'.i18n,\n                    value: hasData && weather.data.rain >= 0\n                        ? '${weather.data.rain.toStringAsFixed(1).padLeft(5)}mm'\n                        : '----',\n                    color: Colors.blue,\n                  ),\n                  _StationChip(\n                    label: '能見度'.i18n,\n                    value: hasData && weather.data.visibility >= 0\n                        ? '${weather.data.visibility.round().toString().padLeft(2)}km'\n                        : '--',\n                    color: Colors.amber,\n                  ),\n                  _StationChip(\n                    label: '風速'.i18n,\n                    value: hasData && weather.data.wind.speed >= 0\n                        ? '${weather.data.wind.direction.isNotEmpty ? '${weather.data.wind.direction} ' : ''}${weather.data.wind.speed.toStringAsFixed(1)}m/s'\n                        : '----',\n                    color: Colors.teal,\n                  ),\n                  _StationChip(\n                    label: '陣風'.i18n,\n                    value: hasData && weather.data.gust.speed >= 0\n                        ? '${weather.data.gust.speed.toStringAsFixed(1).padLeft(4)}m/s'\n                        : '----',\n                    color: Colors.orange,\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildRealtimeInfo() {\n    return [\n      if (GlobalProviders.data.eew.isNotEmpty)\n        ListView.builder(\n          shrinkWrap: true,\n          padding: .zero,\n          physics: const NeverScrollableScrollPhysics(),\n          itemCount: GlobalProviders.data.eew.length,\n          itemBuilder: (context, index) => Padding(\n            padding: const .all(16),\n            child: EewCard(GlobalProviders.data.eew[index]),\n          ),\n        ),\n      if (_thunderstorm != null)\n        Padding(\n          padding: const .all(16),\n          child: ThunderstormCard(_thunderstorm!),\n        ),\n    ];\n  }\n\n  Widget _buildWindCard() {\n    if (_weather == null) return const SizedBox.shrink();\n    return WindCard(_weather!);\n  }\n\n  Widget _buildRadarMap() {\n    return Padding(\n      padding: const .all(16),\n      child: RadarMapCard(key: _mapKey),\n    );\n  }\n\n  Widget _buildForecast() {\n    if (_forecast == null) return const SizedBox.shrink();\n    return ForecastCard(_forecast!);\n  }\n\n  Widget _buildHistoryTimeline() {\n    return ResponsiveContainer(\n      child: Builder(\n        builder: (context) {\n          final history = _history;\n\n          if (history == null || history.isEmpty) {\n            return Column(\n              children: [\n                DateTimelineItem(\n                  TZDateTime.now(UTC).toLocaleFullDateString(context),\n                  first: true,\n                  last: true,\n                  mode: _currentMode,\n                  onModeChanged: _onModeChanged,\n                  isOutOfService: _isOutOfService,\n                ),\n              ],\n            );\n          }\n\n          final grouped = groupBy(\n            history,\n            (e) => e.time.send.toLocaleFullDateString(context),\n          );\n\n          return Column(\n            children: grouped.entries\n                .sorted((a, b) => b.key.compareTo(a.key))\n                .mapIndexed(\n                  (index, entry) => _buildHistoryGroup(entry, index, history),\n                )\n                .toList(),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildHistoryGroup(\n    MapEntry<String, List<History>> entry,\n    int index,\n    List<History> allHistory,\n  ) {\n    final historyGroup = entry.value.sorted(\n      (a, b) => b.time.send.compareTo(a.time.send),\n    );\n\n    return Column(\n      children: [\n        DateTimelineItem(\n          entry.key,\n          first: index == 0,\n          mode: index == 0 ? _currentMode : null,\n          onModeChanged: index == 0 ? _onModeChanged : null,\n          isOutOfService: _isOutOfService,\n        ),\n        ...historyGroup.map((item) {\n          return HistoryTimelineItem(\n            expired: item.isExpired,\n            history: item,\n            last: item == allHistory.last,\n          );\n        }),\n      ],\n    );\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _checkVersion();\n      _measureFirstCard();\n    });\n    GlobalProviders.location.$code.addListener(_refresh);\n    _sheetController.addListener(_onSheetChanged);\n    _refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isVisible = ModalRoute.of(context)?.isCurrent ?? false;\n    if (!_wasVisible && isVisible) {\n      WidgetsBinding.instance.addPostFrameCallback((_) => _refresh());\n    }\n    _wasVisible = isVisible;\n\n    final homeSections = context.select<SettingsUserInterfaceModel, List<HomeDisplaySection>>(\n      (model) => model.homeSections,\n    );\n\n    final utc8Time = WallpaperSelector.getUtc8Time();\n    final wallpaperPath = WallpaperSelector.selectWallpaper(utc8Time);\n    final shaderConfig = ShaderSelector.selectShaderConfig(_weather);\n    final shaderBackground = ShaderSelector.buildShaderBackground(\n      config: shaderConfig,\n      imagePath: wallpaperPath,\n    );\n\n    final screenHeight = context.dimension.height;\n    final baseSnapSize = (_firstCardHeight / screenHeight).clamp(0.25, 0.6);\n    final handleHeight = 28.0 / screenHeight;\n    final minSize = handleHeight.clamp(0.03, 0.05);\n    final snapSizes = [minSize, baseSnapSize, 1.0];\n\n    return Stack(\n      children: [\n        Positioned.fill(\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onVerticalDragUpdate: (details) {\n              if (!_sheetController.isAttached) return;\n              final delta = -details.primaryDelta! / screenHeight;\n              final newSize = (_sheetController.size + delta).clamp(\n                minSize,\n                1.0,\n              );\n              _sheetController.jumpTo(newSize);\n            },\n            onVerticalDragEnd: (details) {\n              if (!_sheetController.isAttached) return;\n              final currentSize = _sheetController.size;\n              final velocity = details.primaryVelocity ?? 0;\n\n              double targetSnap;\n              if (velocity.abs() > 500) {\n                if (velocity > 0) {\n                  targetSnap =\n                      snapSizes.where((s) => s < currentSize).lastOrNull ?? snapSizes.first;\n                } else {\n                  targetSnap =\n                      snapSizes.where((s) => s > currentSize).firstOrNull ?? snapSizes.last;\n                }\n              } else {\n                targetSnap = snapSizes.first;\n                double minDist = (currentSize - targetSnap).abs();\n                for (final snap in snapSizes) {\n                  final dist = (currentSize - snap).abs();\n                  if (dist < minDist) {\n                    minDist = dist;\n                    targetSnap = snap;\n                  }\n                }\n              }\n\n              _sheetController.animateTo(\n                targetSnap,\n                duration: const Duration(milliseconds: 200),\n                curve: Curves.easeOut,\n              );\n            },\n            child: RepaintBoundary(\n              child: shaderBackground,\n            ),\n          ),\n        ),\n        Positioned(\n          top: 0,\n          left: 0,\n          right: 0,\n          child: RepaintBoundary(\n            child: _buildHeroSection(),\n          ),\n        ),\n        Positioned(\n          top: 24,\n          left: 24,\n          child: SafeArea(\n            child: RepaintBoundary(\n              child: Selector<SettingsMapModel, Set<MapLayer>>(\n                selector: (context, model) => model.layers,\n                builder: (context, layers, child) {\n                  return BlurredIconButton(\n                    icon: const Icon(Symbols.map_rounded),\n                    tooltip: '地圖',\n                    onPressed: () => MapRoute(layers: layers.map((l) => l.name).join(',')).push(context),\n                    elevation: 2,\n                  );\n                },\n              ),\n            ),\n          ),\n        ),\n        Positioned(\n          top: 24,\n          right: 24,\n          child: SafeArea(\n            child: RepaintBoundary(\n              child: BlurredIconButton(\n                icon: const Icon(Symbols.settings_rounded),\n                tooltip: '設定',\n                onPressed: () => SettingsIndexRoute().push(context),\n                elevation: 2,\n              ),\n            ),\n          ),\n        ),\n        Positioned(\n          top: 24,\n          left: 0,\n          right: 0,\n          child: SafeArea(\n            child: RepaintBoundary(\n              child: Align(\n                alignment: .topCenter,\n                child: const LocationButton(),\n              ),\n            ),\n          ),\n        ),\n        ValueListenableBuilder<double>(\n          valueListenable: _blurNotifier,\n          builder: (context, blurAmount, child) {\n            if (blurAmount <= 0) return const SizedBox.shrink();\n            final filter = ImageFilter.blur(\n              sigmaX: blurAmount,\n              sigmaY: blurAmount,\n              tileMode: TileMode.clamp,\n            );\n            return Positioned.fill(\n              child: IgnorePointer(\n                child: RepaintBoundary(\n                  child: ClipRect(\n                    child: BackdropFilter(\n                      filter: filter,\n                      blendMode: BlendMode.srcOver,\n                      child: ColoredBox(\n                        color: Colors.black.withValues(\n                          alpha: blurAmount / 50,\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n        _buildDraggableSheet(homeSections),\n      ],\n    );\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    final model = context.read<HomeLocationModel>();\n    if (_homeLocationModel != model) {\n      _homeLocationModel?.removeListener(_refresh);\n      _homeLocationModel = model;\n      model.addListener(_refresh);\n    }\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    if (state == .resumed) _refresh();\n  }\n\n  @override\n  void dispose() {\n    _homeLocationModel?.removeListener(_refresh);\n    _sheetController.removeListener(_onSheetChanged);\n    _blurNotifier.dispose();\n    _isFullyExpandedNotifier.dispose();\n    WidgetsBinding.instance.removeObserver(this);\n    GlobalProviders.location.$code.removeListener(_refresh);\n    _sheetController.dispose();\n    super.dispose();\n  }\n}\n\n/// A single info chip in the station summary row of the home page.\n///\n/// Renders a blurred glass container with a [label] above a [value], tinted\n/// with [color].\nclass _StationChip extends StatelessWidget {\n  final String label;\n  final String value;\n  final Color color;\n\n  const _StationChip({\n    required this.label,\n    required this.value,\n    required this.color,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ClipRRect(\n      borderRadius: .circular(8),\n      child: BackdropFilter(\n        filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),\n        child: Container(\n          padding: const .symmetric(horizontal: 6, vertical: 4),\n          decoration: BoxDecoration(\n            color: color.withValues(alpha: 0.15),\n            borderRadius: .circular(8),\n            border: Border.all(\n              color: context.theme.brightness == .dark\n                  ? Colors.white.withValues(alpha: 0.25)\n                  : const Color.fromARGB(255, 0, 0, 0).withValues(alpha: 0.25),\n              width: 0.5,\n            ),\n          ),\n          child: Column(\n            mainAxisSize: .min,\n            children: [\n              Text(\n                label,\n                style: context.texts.labelSmall?.copyWith(\n                  color: context.theme.brightness == .dark\n                      ? Colors.white\n                      : const Color.fromARGB(255, 90, 90, 90),\n                  fontWeight: .w700,\n                  fontSize: 9,\n                ),\n              ),\n              Text(\n                value,\n                style: context.texts.bodySmall?.copyWith(\n                  color: context.theme.brightness == .dark\n                      ? Colors.white\n                      : const Color.fromARGB(255, 60, 60, 60),\n                  fontWeight: .w600,\n                  fontSize: 12,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/manager.dart",
    "content": "/// Base class for all map layer managers used in the DPIP map feature.\nlibrary;\n\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Manages the lifecycle and visibility of a single map overlay layer.\n///\n/// Subclasses implement [setup], [show], [hide], and [remove] to control\n/// their respective MapLibre layers. Call [build] to obtain any associated\n/// overlay UI (e.g. bottom sheets and legends).\nabstract class MapLayerManager {\n  /// The [BuildContext] used for reading providers and theme data.\n  final BuildContext context;\n\n  /// The MapLibre controller used to add, show, hide, and remove layers.\n  final MapLibreMapController controller;\n\n  /// Whether [setup] has been called and completed successfully.\n  bool didSetup = false;\n\n  /// Whether this layer is currently visible on the map.\n  bool visible = false;\n\n  /// Whether the page is allowed to pop when the back button is pressed.\n  ///\n  /// Return `false` to intercept the pop and handle it via [onPopInvoked].\n  bool get shouldPop => true;\n\n  /// Creates a manager bound to the given [context] and [controller].\n  MapLayerManager(this.context, this.controller);\n\n  /// Initialises map sources and layers, then sets [didSetup] to `true`.\n  Future<void> setup();\n\n  /// Called on every tick of the page timer. Override to refresh layer data.\n  void tick() {}\n\n  /// Hides this layer without removing its underlying sources.\n  Future<void> hide();\n\n  /// Makes this layer visible.\n  Future<void> show();\n\n  /// Completely removes this layer and its sources from the map.\n  Future<void> remove();\n\n  /// Releases any resources held by this manager.\n  void dispose() {}\n\n  /// Called when the user triggers a back-navigation and [shouldPop] is\n  /// `false`. Override to handle custom pop behaviour (e.g. deselecting an\n  /// item).\n  void onPopInvoked() {}\n\n  /// Builds the overlay UI associated with this layer.\n  ///\n  /// Returns an empty [SizedBox] by default.\n  Widget build(BuildContext context) => const SizedBox.shrink();\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/lightning.dart",
    "content": "/// Map layer manager and associated UI for lightning data.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/lightning.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\n\n/// Manages the lightning overlay layer on the DPIP map.\nclass LightningMapLayerManager extends MapLayerManager {\n  /// Creates a [LightningMapLayerManager] bound to [context] and [controller].\n  LightningMapLayerManager(super.context, super.controller);\n\n  /// The currently displayed lightning observation time string.\n  final currentLightningTime = ValueNotifier<String?>(\n    GlobalProviders.data.lightning.firstOrNull,\n  );\n\n  /// Whether a time-change operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  DateTime? _lastFetchTime;\n\n  /// Called with the new time string whenever the displayed time changes.\n  Function(String)? onTimeChanged;\n\n  /// Switches the displayed lightning data to [time].\n  ///\n  /// Does nothing if [time] is already current or a load is in progress.\n  Future<void> setLightningTime(String time) async {\n    if (currentLightningTime.value == time || isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      await remove();\n      currentLightningTime.value = time;\n      await setup();\n\n      onTimeChanged?.call(time);\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'LightningMapLayerManager.setLightningTime',\n        e,\n        s,\n      );\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future<void> _focus() async {\n    try {\n      final location = GlobalProviders.location.coordinates;\n\n      if (location != null && location.isValid) {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(location, 7.4),\n        );\n      } else {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n        );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager._focus', e, s);\n    }\n  }\n\n  Future<void> _fetchData() async {\n    try {\n      final lightningList = (await ExpTech().getLightningList()).reversed.toList();\n      if (!context.mounted) return;\n\n      GlobalProviders.data.setLightning(lightningList);\n      currentLightningTime.value ??= lightningList.first;\n      _lastFetchTime = DateTime.now();\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager._fetchData', e, s);\n    }\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    try {\n      if (GlobalProviders.data.lightning.isEmpty) await _fetchData();\n\n      final time = currentLightningTime.value;\n\n      if (time == null) throw Exception('Time is null');\n\n      final sourceId = MapSourceIds.lightning(time);\n      final layerId = MapLayerIds.lightning(time);\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (!isSourceExists) {\n        late final List<Lightning> lightningData;\n\n        if (GlobalProviders.data.lightningData.containsKey(time)) {\n          lightningData = GlobalProviders.data.lightningData[time]!;\n        } else {\n          lightningData = await ExpTech().getLightning(time);\n          GlobalProviders.data.setLightningData(time, lightningData);\n        }\n\n        final currentTime = int.parse(time);\n        final features = lightningData.map((data) => data.toFeatureBuilder(currentTime)).toList();\n\n        final data = GeoJsonBuilder().setFeatures(features).build();\n\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        final properties = SymbolLayerProperties(\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.1,\n            15,\n            0.8,\n          ],\n          iconImage: [\n            Expressions.match,\n            ['get', 'type'],\n            '1-5',\n            'lightning-1-5',\n            '1-10',\n            'lightning-1-10',\n            '1-30',\n            'lightning-1-30',\n            '1-60',\n            'lightning-1-60',\n            '0-5',\n            'lightning-0-5',\n            '0-10',\n            'lightning-0-10',\n            '0-30',\n            'lightning-0-30',\n            '0-60',\n            'lightning-0-60',\n            'lightning-0-60',\n          ],\n          iconOpacity: 0.75,\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      if (isSourceExists && isLayerExists) return;\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    final time = currentLightningTime.value;\n    if (time == null) return;\n\n    final layerId = MapLayerIds.lightning(time);\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final time = currentLightningTime.value;\n    if (time == null) return;\n\n    final layerId = MapLayerIds.lightning(time);\n\n    try {\n      await controller.setLayerVisibility(layerId, true);\n\n      await _focus();\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5)\n        await _fetchData();\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final time = currentLightningTime.value;\n      if (time == null) return;\n\n      final layerId = MapLayerIds.lightning(time);\n      final sourceId = MapSourceIds.lightning(time);\n\n      await controller.removeLayer(layerId);\n\n      await controller.removeSource(sourceId);\n    } catch (e, s) {\n      TalkerManager.instance.error('LightningMapLayerManager.remove', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  Widget build(BuildContext context) => LightningMapLayerSheet(manager: this);\n}\n\n/// The bottom sheet and legend overlay for the lightning layer.\nclass LightningMapLayerSheet extends StatelessWidget {\n  /// The [LightningMapLayerManager] whose state drives this sheet.\n  final LightningMapLayerManager manager;\n\n  /// Creates a [LightningMapLayerSheet] for the given [manager].\n  const LightningMapLayerSheet({super.key, required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        MorphingSheet(\n          title: '閃電'.i18n,\n          borderRadius: .circular(16),\n          elevation: 4,\n          partialBuilder: (context, controller, sheetController) {\n            return Padding(\n              padding: const .symmetric(vertical: 8),\n              child: Selector<DpipDataModel, UnmodifiableListView<String>>(\n                selector: (context, model) => model.lightning,\n                builder: (context, lightning, child) {\n                  final times = lightning.map((time) {\n                    final t = time.toSimpleDateTimeString().split(' ');\n                    return (date: t[0], time: t[1], value: time);\n                  });\n                  final grouped = times.groupListsBy((time) => time.date).entries.toList();\n\n                  return Column(\n                    mainAxisSize: .min,\n                    children: [\n                      Padding(\n                        padding: const .symmetric(\n                          horizontal: 16,\n                          vertical: 8,\n                        ),\n                        child: Row(\n                          spacing: 8,\n                          children: [\n                            const Icon(Symbols.bolt_rounded, size: 24),\n                            Text('閃電'.i18n, style: context.texts.titleMedium),\n                          ],\n                        ),\n                      ),\n                      SizedBox(\n                        height: kMinInteractiveDimension,\n                        child: ValueListenableBuilder<String?>(\n                          valueListenable: manager.currentLightningTime,\n                          builder: (context, currentLightningTime, child) {\n                            return ListView.builder(\n                              padding: const .symmetric(\n                                horizontal: 16,\n                              ),\n                              scrollDirection: .horizontal,\n                              physics: const AlwaysScrollableScrollPhysics(),\n                              itemCount: grouped.length,\n                              itemBuilder: (context, index) {\n                                final MapEntry(key: date, value: group) = grouped[index];\n\n                                final children = <Widget>[Text(date)];\n\n                                for (final time in group) {\n                                  final isSelected = time.value == currentLightningTime;\n\n                                  children.add(\n                                    ValueListenableBuilder<bool>(\n                                      valueListenable: manager.isLoading,\n                                      builder: (context, isLoading, child) {\n                                        return FilterChip(\n                                          selected: isSelected,\n                                          showCheckmark: !isLoading,\n                                          label: Text(time.time),\n                                          side: BorderSide(\n                                            color: isSelected\n                                                ? context.colors.primary\n                                                : context.colors.outlineVariant,\n                                          ),\n                                          avatar: isSelected && isLoading\n                                              ? const LoadingIcon()\n                                              : null,\n                                          onSelected: isLoading\n                                              ? null\n                                              : (selected) {\n                                                  if (!selected) return;\n                                                  manager.setLightningTime(\n                                                    time.value,\n                                                  );\n                                                },\n                                        );\n                                      },\n                                    ),\n                                  );\n                                }\n\n                                children.add(\n                                  const Padding(\n                                    padding: .only(right: 8),\n                                    child: VerticalDivider(\n                                      width: 16,\n                                      indent: 8,\n                                      endIndent: 8,\n                                    ),\n                                  ),\n                                );\n\n                                return Row(\n                                  mainAxisSize: .min,\n                                  spacing: 8,\n                                  children: children,\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ),\n                    ],\n                  );\n                },\n              ),\n            );\n          },\n        ),\n        Positioned(\n          top: 24 + 48 + 16,\n          left: 24,\n          child: SafeArea(\n            child: BlurredContainer(\n              elevation: 4,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n              child: Legend(\n                items: [\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.health_cross_rounded,\n                      fill: Color(0xffff0000),\n                      size: 20,\n                    ),\n                    label: '5 分鐘內對地閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.health_cross_rounded,\n                      fill: Color(0xffffff00),\n                      size: 20,\n                    ),\n                    label: '10 分鐘內對地閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.health_cross_rounded,\n                      fill: Color(0xff00ff00),\n                      size: 20,\n                    ),\n                    label: '30 分鐘內對地閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.health_cross_rounded,\n                      fill: Color(0xff0000ff),\n                      size: 20,\n                    ),\n                    label: '60 分鐘內對地閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.circle_rounded,\n                      fill: Color(0xffff0000),\n                      size: 20,\n                    ),\n                    label: '5 分鐘內雲間閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.circle_rounded,\n                      fill: Color(0xffffff00),\n                      size: 20,\n                    ),\n                    label: '10 分鐘內雲間閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.circle_rounded,\n                      fill: Color(0xff00ff00),\n                      size: 20,\n                    ),\n                    label: '30 分鐘內雲間閃電'.i18n,\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.circle_rounded,\n                      fill: Color(0xff0000ff),\n                      size: 20,\n                    ),\n                    label: '60 分鐘內雲間閃電'.i18n,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/monitor.dart",
    "content": "/// Map layer manager and associated UI for the real-time seismic monitor.\nlibrary;\n\nimport 'dart:async';\nimport 'dart:collection';\n\nimport 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/core/eew.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/models/settings/map.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/instrumental_intensity_color.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/intensity_legend.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:flutter/material.dart';\nimport 'package:geojson_vi/geojson_vi.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_text/styled_text.dart';\n\n/// Manages the real-time seismic monitor overlay, EEW circles, and detection\n/// boxes on the DPIP map.\nclass MonitorMapLayerManager extends MapLayerManager {\n  /// Whether this manager is replaying historical data rather than live data.\n  final bool isReplayMode;\n\n  /// The Unix timestamp (ms) used when replaying historical data.\n  final int? replayTimestamp;\n\n  Timer? _blinkTimer;\n  bool _isBoxVisible = true;\n  bool _isEpicenterVisible = true;\n  Timer? _focusTimer;\n  bool _isFocusing = false;\n\n  /// The padding in logical pixels applied to each side when fitting the camera\n  /// to detection bounds.\n  static const double kCameraPadding = 80.0;\n\n  /// Vertical offset of the first text label line above the station circle.\n  static const double kLabelBaseOffset = 0.8;\n\n  /// Vertical spacing between consecutive text label lines.\n  static const double kLabelLineHeight = 1.2;\n\n  /// Whether data has been received within the last 12 seconds.\n  bool get dataStatus => _dataStatus();\n\n  /// The most recently measured round-trip latency in milliseconds.\n  double get ping => _ping;\n  double _ping = 0;\n\n  // Cached bounds for performance optimization\n  List<LatLng>? _cachedBounds;\n  int? _lastRtsTime;\n  LatLngBounds? _lastZoomBounds;\n\n  // Cached GeoJSON data - updated by _onDataChanged, consumed by tick\n  Map<String, dynamic>? _cachedRtsGeoJson;\n  Map<String, dynamic>? _cachedIntensityGeoJson;\n  Map<String, dynamic>? _cachedBoxGeoJson;\n  bool _needsRtsUpdate = false;\n\n  bool _isUpdatingEew = false;\n  bool _hasActiveEew = false;\n  String? _lastEewId;\n  int? _lastEewSerial;\n\n  late final String _rtsSourceId = MapSourceIds.rts();\n  late final String _rtsLayerId = MapLayerIds.rts();\n  late final String _intensitySourceId = MapSourceIds.intensity();\n  late final String _intensityLayerId = MapLayerIds.intensity();\n  late final String _intensity0SourceId = MapSourceIds.intensity0();\n  late final String _intensity0LayerId = MapLayerIds.intensity0();\n  late final String _boxSourceId = MapSourceIds.box();\n  late final String _boxLayerId = MapLayerIds.box();\n  late final String _eewSourceId = MapSourceIds.eew();\n  late final String _epicenterLayerId = MapLayerIds.eew('x');\n  late final String _pWaveLayerId = MapLayerIds.eew('p');\n  late final String _sWaveLayerId = MapLayerIds.eew('s');\n\n  /// Creates a [MonitorMapLayerManager].\n  ///\n  /// Set [isReplayMode] to `true` and supply a [replayTimestamp] to replay\n  /// historical data instead of the live feed.\n  MonitorMapLayerManager(\n    super.context,\n    super.controller, {\n    this.isReplayMode = false,\n    this.replayTimestamp = 1762892804468,\n  }) {\n    GlobalProviders.data.setReplayMode(isReplayMode, replayTimestamp);\n    _setupBlinkTimer();\n  }\n\n  bool _dataStatus() {\n    return (GlobalProviders.data.currentTime - (_lastDataReceivedTime ?? 0)) < 12000;\n  }\n\n  /// The timestamp of the most recently processed RTS data packet.\n  final currentRtsTime = ValueNotifier<int?>(GlobalProviders.data.syncTime);\n\n  /// The human-readable time string shown in the monitor UI.\n  final displayTimeNotifier = ValueNotifier<String>('N/A');\n\n  /// The round-trip latency value exposed for the monitor UI.\n  final pingNotifier = ValueNotifier<double>(0);\n  int? _lastDataReceivedTime;\n  int _lastDisplayedSecond = 0;\n\n  /// MapLibre expression that maps instrumental intensity values to colours.\n  static final kRtsCircleColor = [\n    Expressions.interpolate,\n    ['linear'],\n    [Expressions.get, 'i'],\n    -3,\n    InstrumentalIntensityColor.intensity_3.toHexStringRGB(),\n    -2,\n    InstrumentalIntensityColor.intensity_2.toHexStringRGB(),\n    -1,\n    InstrumentalIntensityColor.intensity_1.toHexStringRGB(),\n    0,\n    InstrumentalIntensityColor.intensity0.toHexStringRGB(),\n    1,\n    InstrumentalIntensityColor.intensity1.toHexStringRGB(),\n    2,\n    InstrumentalIntensityColor.intensity2.toHexStringRGB(),\n    3,\n    InstrumentalIntensityColor.intensity3.toHexStringRGB(),\n    4,\n    InstrumentalIntensityColor.intensity4.toHexStringRGB(),\n    5,\n    InstrumentalIntensityColor.intensity5.toHexStringRGB(),\n    6,\n    InstrumentalIntensityColor.intensity6.toHexStringRGB(),\n    7,\n    InstrumentalIntensityColor.intensity7.toHexStringRGB(),\n  ];\n\n  /// MapLibre expression that scales RTS circles based on zoom level.\n  static const kRtsCircleRadius = [\n    Expressions.interpolate,\n    ['linear'],\n    [Expressions.zoom],\n    4,\n    2,\n    12,\n    8,\n  ];\n\n  void _setupBlinkTimer() {\n    _blinkTimer?.cancel();\n    _blinkTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {\n      if (!didSetup) return;\n\n      try {\n        // Cache blink conditions at the start of each blink cycle to ensure consistency\n        final hasBoxData = (_cachedBoxGeoJson?['features'] as List?)?.isNotEmpty ?? false;\n        final hasBoxFlag = GlobalProviders.data.rts?.box.isNotEmpty ?? false;\n        final shouldBlinkBoxes = hasBoxData && hasBoxFlag;\n\n        final hasEew = GlobalProviders.data.activeEew.isNotEmpty;\n        final shouldBlinkEpicenter = hasEew;\n\n        // Boxes blinking - only toggle if conditions allow, otherwise hide\n        if (shouldBlinkBoxes) {\n          _isBoxVisible = !_isBoxVisible;\n          await controller.setLayerVisibility(_boxLayerId, _isBoxVisible);\n        } else {\n          _isBoxVisible = false;\n          await controller.setLayerVisibility(_boxLayerId, false);\n        }\n\n        // Epicenter blinking - independent of boxes\n        if (shouldBlinkEpicenter) {\n          _isEpicenterVisible = !_isEpicenterVisible;\n          await controller.setLayerVisibility(\n            _epicenterLayerId,\n            _isEpicenterVisible,\n          );\n        } else {\n          _isEpicenterVisible = false;\n          await controller.setLayerVisibility(_epicenterLayerId, false);\n        }\n      } catch (e, s) {\n        TalkerManager.instance.error(\n          'MonitorMapLayerManager._blinkTimer',\n          e,\n          s,\n        );\n      }\n    });\n  }\n\n  /// Extracts all coordinates from detection areas (GeoJSON polygons) that are present\n  /// in the current RTS data. Uses caching to avoid recalculating bounds when\n  /// RTS data hasn't changed. Returns empty list if no detection areas exist.\n  List<LatLng> _getFocusBounds() {\n    final rts = GlobalProviders.data.rts;\n    if (_cachedBounds != null && _lastRtsTime == rts?.time) return _cachedBounds!;\n\n    final coords = (rts?.box.isEmpty ?? true)\n        ? <LatLng>[]\n        : [\n            for (final area in Global.boxGeojson.features)\n              if (area?.properties?['ID'] case final id when rts!.box.containsKey(id.toString()))\n                for (final coord in (area!.geometry! as GeoJSONPolygon).coordinates[0] as List)\n                  LatLng(\n                    (coord[1] as num).toDouble(),\n                    (coord[0] as num).toDouble(),\n                  ),\n          ];\n\n    _cachedBounds = coords;\n    _lastRtsTime = rts?.time;\n    return coords;\n  }\n\n  /// Checks if the new bounds have changed significantly from the last zoom bounds\n  /// Only zooms if the change is greater than 10% in any dimension\n  bool _shouldZoomToBounds(LatLngBounds newBounds) {\n    final lastBounds = _lastZoomBounds;\n    if (lastBounds == null) return true;\n\n    final (latDiff, lngDiff) = (\n      (newBounds.northeast.latitude - newBounds.southwest.latitude).abs(),\n      (newBounds.northeast.longitude - newBounds.southwest.longitude).abs(),\n    );\n    final (lastLatDiff, lastLngDiff) = (\n      (lastBounds.northeast.latitude - lastBounds.southwest.latitude).abs(),\n      (lastBounds.northeast.longitude - lastBounds.southwest.longitude).abs(),\n    );\n\n    const minBoundSize = 0.0001; // ~11 meters - safety check for division by zero\n    if (lastLatDiff < minBoundSize || lastLngDiff < minBoundSize) return true;\n\n    return (latDiff - lastLatDiff).abs() / lastLatDiff > 0.1 ||\n        (lngDiff - lastLngDiff).abs() / lastLngDiff > 0.1;\n  }\n\n  /// Calculates the bounding box from coordinates and animates the map camera\n  /// to fit all detection areas with padding. Only zooms if the bounds have\n  /// changed significantly (>10%) from the last zoom to prevent excessive zooming.\n  Future<void> _updateMapBounds(List<LatLng> coordinates) async {\n    if (coordinates.isEmpty) return;\n\n    var (minLat, maxLat, minLng, maxLng) = (\n      coordinates[0].latitude,\n      coordinates[0].latitude,\n      coordinates[0].longitude,\n      coordinates[0].longitude,\n    );\n\n    for (var i = 1; i < coordinates.length; i++) {\n      final (lat, lng) = (coordinates[i].latitude, coordinates[i].longitude);\n      if (lat < minLat) {\n        minLat = lat;\n      } else if (lat > maxLat) {\n        maxLat = lat;\n      }\n      if (lng < minLng) {\n        minLng = lng;\n      } else if (lng > maxLng) {\n        maxLng = lng;\n      }\n    }\n\n    final bounds = LatLngBounds(\n      southwest: LatLng(minLat, minLng),\n      northeast: LatLng(maxLat, maxLng),\n    );\n    if (!_shouldZoomToBounds(bounds)) return;\n\n    await controller.animateCamera(\n      CameraUpdate.newLatLngBounds(\n        bounds,\n        left: kCameraPadding,\n        top: kCameraPadding,\n        right: kCameraPadding,\n        bottom: kCameraPadding,\n      ),\n      duration: const Duration(milliseconds: 500),\n    );\n    _lastZoomBounds = bounds;\n  }\n\n  Future<void> _focusReset() => controller.animateCamera(\n    CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n  );\n\n  /// Automatically adjusts the map camera to fit all detection areas (boxes)\n  /// when RTS data contains detection zones. Runs every 2 seconds when enabled.\n  /// If no detection areas exist, resets to the default Taiwan center view.\n  /// Uses caching and debouncing to optimize performance.\n  Future<void> _autoFocus() async {\n    final autoZoomEnabled = context.read<SettingsMapModel>().autoZoom;\n    if (!visible || !context.mounted || !autoZoomEnabled) return;\n\n    try {\n      final bounds = _getFocusBounds();\n      if (bounds.isNotEmpty) {\n        await _updateMapBounds(bounds);\n      } else {\n        await _focusReset();\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager._autoFocus', e, s);\n    }\n  }\n\n  void _startFocusTimer() {\n    _focusTimer?.cancel();\n    _focusTimer = Timer.periodic(const Duration(seconds: 2), (_) async {\n      if (!_isFocusing) {\n        _isFocusing = true;\n        await _autoFocus();\n        _isFocusing = false;\n      }\n    });\n  }\n\n  void _stopFocusTimer() {\n    _focusTimer?.cancel();\n    _focusTimer = null;\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    final colors = context.colors;\n\n    try {\n      // Single batch query for sources and layers\n      final results = await Future.wait([\n        controller.getSourceIds(),\n        controller.getLayerIds(),\n      ]);\n      final sources = results[0];\n      final layers = results[1];\n\n      final rtsSourceId = MapSourceIds.rts();\n      final rtsLayerId = MapLayerIds.rts();\n      final intensitySourceId = MapSourceIds.intensity();\n      final intensityLayerId = MapLayerIds.intensity();\n      final intensity0SourceId = MapSourceIds.intensity0();\n      final intensity0LayerId = MapLayerIds.intensity0();\n      final boxSourceId = MapSourceIds.box();\n      final boxLayerId = MapLayerIds.box();\n      final eewSourceId = MapSourceIds.eew();\n      final epicenterLayerId = MapLayerIds.eew('x');\n      final pWaveLayerId = MapLayerIds.eew('p');\n      final sWaveLayerId = MapLayerIds.eew('s');\n\n      final existingSources = sources.toSet();\n      final existingLayers = layers.toSet();\n\n      // Check layer existence\n      final isRtsLayerExists = existingLayers.contains(rtsLayerId);\n      final isIntensity0LayerExists = existingLayers.contains(\n        intensity0LayerId,\n      );\n      final isIntensityLayerExists = existingLayers.contains(intensityLayerId);\n      final isBoxLayerExists = existingLayers.contains(boxLayerId);\n      final isEewLayerExists =\n          existingLayers.contains(epicenterLayerId) &&\n          existingLayers.contains(pWaveLayerId) &&\n          existingLayers.contains(sWaveLayerId);\n\n      if (!context.mounted) return;\n\n      // Prepare GeoJSON data (reuse intensity data)\n      final intensityData = GlobalProviders.data.getIntensityGeoJson();\n      final sourceAdditions = <Future<void>>[];\n\n      if (!existingSources.contains(rtsSourceId)) {\n        sourceAdditions.add(\n          controller.addSource(\n            rtsSourceId,\n            GeojsonSourceProperties(data: GlobalProviders.data.getRtsGeoJson()),\n          ),\n        );\n      }\n\n      if (!existingSources.contains(intensity0SourceId)) {\n        sourceAdditions.add(\n          controller.addSource(\n            intensity0SourceId,\n            GeojsonSourceProperties(data: intensityData),\n          ),\n        );\n      }\n\n      if (!existingSources.contains(intensitySourceId)) {\n        sourceAdditions.add(\n          controller.addSource(\n            intensitySourceId,\n            GeojsonSourceProperties(data: intensityData),\n          ),\n        );\n      }\n\n      if (!existingSources.contains(boxSourceId)) {\n        sourceAdditions.add(\n          controller.addSource(\n            boxSourceId,\n            GeojsonSourceProperties(data: GlobalProviders.data.getBoxGeoJson()),\n          ),\n        );\n      }\n\n      if (!existingSources.contains(eewSourceId)) {\n        sourceAdditions.add(\n          controller.addSource(\n            eewSourceId,\n            GeojsonSourceProperties(data: GlobalProviders.data.getEewGeoJson()),\n          ),\n        );\n      }\n\n      // Add all sources in parallel\n      if (sourceAdditions.isNotEmpty) {\n        await Future.wait(sourceAdditions);\n      }\n\n      if (!context.mounted) return;\n\n      if (!isRtsLayerExists) {\n        final properties = CircleLayerProperties(\n          circleColor: kRtsCircleColor,\n          circleRadius: kRtsCircleRadius,\n          circleOpacity: [\n            Expressions.caseExpression,\n            [Expressions.has, 'i'],\n            1,\n            0,\n          ],\n          circleStrokeColor: context.colors.outlineVariant.toHexStringRGB(),\n          circleStrokeWidth: 1,\n          circleSortKey: [\n            Expressions.coalesce,\n            [Expressions.get, 'i'],\n            -5,\n          ],\n          visibility: visible ? 'visible' : 'none',\n        );\n        // Note: Previously this used Expressions.format with inline font/styles and '\\n'.\n        // After the map package upgrade, multi-line formatting via '\\n' became unreliable,\n        // so we render each logical text line as its own SymbolLayer and stack them using\n        // `textOffset` with the constants above.\n\n        final labelIdProps = SymbolLayerProperties(\n          textField: [Expressions.get, 'id'],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [0, kLabelBaseOffset],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelLocProps = SymbolLayerProperties(\n          textField: [\n            Expressions.caseExpression,\n            [\n              Expressions.all,\n              [Expressions.has, 'city'],\n              [Expressions.has, 'town'],\n            ],\n            [\n              Expressions.concat,\n              [Expressions.get, 'city'],\n              ' ',\n              [Expressions.get, 'town'],\n            ],\n            '海外測站'.i18n,\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Regular'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 1],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelDetailIProps = SymbolLayerProperties(\n          textField: [\n            Expressions.caseExpression,\n            [Expressions.has, 'i'],\n            [\n              Expressions.concat,\n              '即時震度：'.i18n,\n              [Expressions.get, 'i'],\n            ],\n            '無資料'.i18n,\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Regular'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 2],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelDetailPgaProps = SymbolLayerProperties(\n          textField: [\n            Expressions.caseExpression,\n            [Expressions.has, 'pga'],\n            [\n              Expressions.concat,\n              '地動加速度：'.i18n,\n              [Expressions.get, 'pga'],\n              'gal',\n            ],\n            '',\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Regular'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 3],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelDetailPgvProps = SymbolLayerProperties(\n          textField: [\n            Expressions.caseExpression,\n            [Expressions.has, 'pgv'],\n            [\n              Expressions.concat,\n              '地動速度：'.i18n,\n              [Expressions.get, 'pgv'],\n              'cm/s',\n            ],\n            '',\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Regular'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 4],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final layerAdditions = <Future<void>>[\n          controller.addLayer(\n            rtsSourceId,\n            rtsLayerId,\n            properties,\n            belowLayerId: BaseMapLayerIds.userLocation,\n          ),\n          controller.addLayer(\n            rtsSourceId,\n            '$rtsLayerId-label-id',\n            labelIdProps,\n            belowLayerId: BaseMapLayerIds.userLocation,\n            minzoom: 10,\n          ),\n          controller.addLayer(\n            rtsSourceId,\n            '$rtsLayerId-label-loc',\n            labelLocProps,\n            belowLayerId: BaseMapLayerIds.userLocation,\n            minzoom: 10,\n          ),\n          controller.addLayer(\n            rtsSourceId,\n            '$rtsLayerId-label-detail-i',\n            labelDetailIProps,\n            belowLayerId: BaseMapLayerIds.userLocation,\n            minzoom: 10,\n          ),\n          controller.addLayer(\n            rtsSourceId,\n            '$rtsLayerId-label-detail-pga',\n            labelDetailPgaProps,\n            belowLayerId: BaseMapLayerIds.userLocation,\n            minzoom: 10,\n          ),\n          controller.addLayer(\n            rtsSourceId,\n            '$rtsLayerId-label-detail-pgv',\n            labelDetailPgvProps,\n            belowLayerId: BaseMapLayerIds.userLocation,\n            minzoom: 10,\n          ),\n        ];\n\n        await Future.wait(layerAdditions);\n      }\n\n      if (!isIntensity0LayerExists) {\n        final properties = CircleLayerProperties(\n          circleColor: Colors.grey.toHexStringRGB(),\n          circleRadius: kRtsCircleRadius,\n          circleOpacity: [\n            Expressions.caseExpression,\n            [Expressions.has, 'intensity'],\n            [\n              Expressions.caseExpression,\n              [\n                Expressions.all,\n                [\n                  Expressions.equal,\n                  [Expressions.get, 'intensity'],\n                  0,\n                ],\n                [\n                  Expressions.equal,\n                  [Expressions.get, 'alert'],\n                  1,\n                ],\n              ],\n              1,\n              0,\n            ],\n            0,\n          ],\n          visibility: 'none',\n        );\n\n        await controller.addLayer(\n          intensity0SourceId,\n          intensity0LayerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      if (!isIntensityLayerExists) {\n        const properties = SymbolLayerProperties(\n          symbolSortKey: [Expressions.get, 'intensity'],\n          symbolZOrder: 'source',\n          iconSize: kSymbolIconSize,\n          iconImage: [\n            Expressions.match,\n            [Expressions.get, 'intensity'],\n            1,\n            'intensity-1',\n            2,\n            'intensity-2',\n            3,\n            'intensity-3',\n            4,\n            'intensity-4',\n            5,\n            'intensity-5',\n            6,\n            'intensity-6',\n            7,\n            'intensity-7',\n            8,\n            'intensity-8',\n            9,\n            'intensity-9',\n            '',\n          ],\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          visibility: 'none',\n        );\n\n        await controller.addLayer(\n          intensitySourceId,\n          intensityLayerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      if (!isBoxLayerExists) {\n        const properties = LineLayerProperties(\n          lineWidth: 2,\n          lineColor: [\n            Expressions.match,\n            [Expressions.get, 'i'],\n            9,\n            '#FF0000',\n            8,\n            '#FF0000',\n            7,\n            '#FF0000',\n            6,\n            '#FF0000',\n            5,\n            '#FF0000',\n            4,\n            '#FF0000',\n            3,\n            '#EAC100',\n            2,\n            '#EAC100',\n            1,\n            '#00DB00',\n            '#00DB00',\n          ],\n          visibility: 'none',\n          lineSortKey: [Expressions.get, 'i'],\n        );\n\n        await controller.addLayer(\n          boxSourceId,\n          boxLayerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      if (!isEewLayerExists) {\n        final pWaveProperties = LineLayerProperties(\n          lineColor: Colors.cyan.toHexStringRGB(),\n          lineWidth: 2,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          eewSourceId,\n          pWaveLayerId,\n          pWaveProperties,\n          enableInteraction: false,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          filter: [\n            Expressions.equal,\n            [Expressions.get, 'type'],\n            'p',\n          ],\n        );\n\n        final sWaveProperties = LineLayerProperties(\n          lineColor: Colors.red.toHexStringRGB(),\n          lineWidth: 2,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          eewSourceId,\n          sWaveLayerId,\n          sWaveProperties,\n          enableInteraction: false,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          filter: [\n            Expressions.equal,\n            [Expressions.get, 'type'],\n            's',\n          ],\n        );\n\n        final epicenterProperties = SymbolLayerProperties(\n          iconImage: 'cross-7',\n          iconSize: kSymbolIconSize,\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          symbolZOrder: 'source',\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          eewSourceId,\n          epicenterLayerId,\n          epicenterProperties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          filter: [\n            Expressions.equal,\n            [Expressions.get, 'type'],\n            'x',\n          ],\n        );\n      }\n\n      didSetup = true;\n      _startFocusTimer();\n      GlobalProviders.data.addListener(_onDataChanged);\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  void tick() {\n    if (!didSetup || !visible) return;\n    _lastDataReceivedTime = GlobalProviders.data.currentTime;\n    final hasActiveEew = GlobalProviders.data.activeEew.isNotEmpty;\n\n    if (hasActiveEew && !_isUpdatingEew) {\n      _isUpdatingEew = true;\n      unawaited(_updateEewFromCache());\n    } else if (!hasActiveEew && _hasActiveEew) {\n      _hasActiveEew = false;\n      if (!_isUpdatingEew) {\n        _isUpdatingEew = true;\n        unawaited(_clearEew());\n      }\n    }\n\n    if (hasActiveEew) {\n      _hasActiveEew = true;\n    }\n\n    if (_needsRtsUpdate) {\n      unawaited(_updateRtsFromCache());\n      _needsRtsUpdate = false;\n    }\n\n    _updateDisplayTime();\n  }\n\n  void _updateDisplayTime() {\n    final currentTime = GlobalProviders.data.currentTime;\n    final currentSecond = currentTime ~/ 1000;\n\n    if (currentSecond == _lastDisplayedSecond) return;\n    _lastDisplayedSecond = currentSecond;\n\n    final lastDataReceivedTime = _lastDataReceivedTime;\n    final isStale = lastDataReceivedTime != null && (currentTime - lastDataReceivedTime) > 3000;\n\n    if (lastDataReceivedTime != null) {\n      if (isStale) {\n        displayTimeNotifier.value = '${lastDataReceivedTime.toFullSimpleDateTimeString()}|STALE';\n      } else {\n        displayTimeNotifier.value = currentTime.toFullSimpleDateTimeString();\n      }\n    }\n\n    final rtsPing = GlobalProviders.data.lastRtsPing;\n    if (rtsPing != null) {\n      _ping = rtsPing.toDouble();\n    } else {\n      _ping = -1;\n    }\n    pingNotifier.value = _ping;\n  }\n\n  void _onDataChanged() {\n    final newRts = GlobalProviders.data.rts;\n    final newRtsTime = newRts?.time;\n\n    if (newRtsTime != currentRtsTime.value) {\n      currentRtsTime.value = newRtsTime;\n      _cachedBounds = null;\n      _lastRtsTime = null;\n      _lastDataReceivedTime = GlobalProviders.data.currentTime;\n\n      _cachedRtsGeoJson = GlobalProviders.data.getRtsGeoJson();\n      _cachedIntensityGeoJson = GlobalProviders.data.getIntensityGeoJson();\n      _cachedBoxGeoJson = GlobalProviders.data.getBoxGeoJson();\n      _needsRtsUpdate = true;\n    }\n  }\n\n  Future<void> _updateRtsFromCache() async {\n    if (!didSetup || _cachedRtsGeoJson == null) return;\n\n    try {\n      final existingSources = (await controller.getSourceIds()).toSet();\n      final hasBox = GlobalProviders.data.rts?.box.isNotEmpty ?? false;\n      final hasRtsData = (_cachedRtsGeoJson?['features'] as List?)?.isNotEmpty ?? false;\n      final hasIntensityData = (_cachedIntensityGeoJson?['features'] as List?)?.isNotEmpty ?? false;\n      final hasBoxData = (_cachedBoxGeoJson?['features'] as List?)?.isNotEmpty ?? false;\n\n      await Future.wait([\n        if (hasRtsData && existingSources.contains(_rtsSourceId))\n          controller.setGeoJsonSource(_rtsSourceId, _cachedRtsGeoJson!),\n        if (hasIntensityData && existingSources.contains(_intensitySourceId))\n          controller.setGeoJsonSource(\n            _intensitySourceId,\n            _cachedIntensityGeoJson!,\n          ),\n        if (hasIntensityData && existingSources.contains(_intensity0SourceId))\n          controller.setGeoJsonSource(\n            _intensity0SourceId,\n            _cachedIntensityGeoJson!,\n          ),\n        if (hasBoxData && existingSources.contains(_boxSourceId))\n          controller.setGeoJsonSource(_boxSourceId, _cachedBoxGeoJson!),\n\n        controller.setLayerVisibility(_rtsLayerId, hasRtsData && !hasBox),\n        controller.setLayerVisibility(\n          '$_rtsLayerId-label-id',\n          hasRtsData && !hasBox,\n        ),\n        controller.setLayerVisibility(\n          '$_rtsLayerId-label-loc',\n          hasRtsData && !hasBox,\n        ),\n        controller.setLayerVisibility(\n          '$_rtsLayerId-label-detail-i',\n          hasRtsData && !hasBox,\n        ),\n        controller.setLayerVisibility(\n          '$_rtsLayerId-label-detail-pga',\n          hasRtsData && !hasBox,\n        ),\n        controller.setLayerVisibility(\n          '$_rtsLayerId-label-detail-pgv',\n          hasRtsData && !hasBox,\n        ),\n        controller.setLayerVisibility(\n          _intensityLayerId,\n          hasIntensityData && hasBox,\n        ),\n        controller.setLayerVisibility(\n          _intensity0LayerId,\n          hasIntensityData && hasBox,\n        ),\n        controller.setLayerVisibility(_boxLayerId, hasBoxData && hasBox),\n      ]);\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'MonitorMapLayerManager._updateRtsFromCache',\n        e,\n        s,\n      );\n    }\n  }\n\n  Future<void> _updateEewFromCache() async {\n    if (!didSetup) {\n      _isUpdatingEew = false;\n      return;\n    }\n\n    try {\n      // Always update wave circles (they change every tick)\n      final data = GlobalProviders.data.getEewGeoJson();\n      await controller.setGeoJsonSource(_eewSourceId, data);\n\n      // Only update town intensity colors when EEW info changes\n      final activeEew = GlobalProviders.data.activeEew;\n      if (activeEew.isNotEmpty) {\n        final eew = activeEew.first;\n        final needsIntensityUpdate = _lastEewId != eew.id || _lastEewSerial != eew.serial;\n\n        if (needsIntensityUpdate) {\n          _lastEewId = eew.id;\n          _lastEewSerial = eew.serial;\n\n          final intensityData = eewAreaPga(\n            eew.info.latitude,\n            eew.info.longitude,\n            eew.info.depth,\n            eew.info.magnitude,\n            Global.location,\n          );\n\n          final colorEntries = <dynamic>[];\n\n          intensityData.forEach((key, value) {\n            if (key == 'max_i') return;\n            final code = int.tryParse(key);\n            if (code == null) return;\n            final intensity = intensityFloatToInt(\n              (value as Map)['i'] as double,\n            );\n            if (intensity > 0) {\n              colorEntries.add(code);\n              colorEntries.add(\n                IntensityColor.intensity(intensity).toHexStringRGB(),\n              );\n            }\n          });\n\n          // Only apply if we have color entries\n          if (colorEntries.isNotEmpty) {\n            final fillColorExpression = <dynamic>[\n              'match',\n              ['get', 'CODE'],\n              ...colorEntries,\n              context.colors.surfaceContainerHigh.toHexStringRGB(),\n            ];\n\n            // Hide county fill to show town colors\n            await controller.setLayerProperties(\n              BaseMapLayerIds.exptechCountyFill,\n              const FillLayerProperties(fillOpacity: 0),\n            );\n\n            await controller.setLayerProperties(\n              BaseMapLayerIds.exptechTownFill,\n              FillLayerProperties(\n                fillColor: fillColorExpression,\n                fillOpacity: 1,\n              ),\n            );\n          }\n        }\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'MonitorMapLayerManager._updateEewFromCache',\n        e,\n        s,\n      );\n    } finally {\n      _isUpdatingEew = false;\n    }\n  }\n\n  Future<void> _clearEew() async {\n    try {\n      final emptyData = {'type': 'FeatureCollection', 'features': []};\n      await controller.setGeoJsonSource(_eewSourceId, emptyData);\n\n      // Reset EEW tracking\n      _lastEewId = null;\n      _lastEewSerial = null;\n\n      // Restore county fill\n      await controller.setLayerProperties(\n        BaseMapLayerIds.exptechCountyFill,\n        FillLayerProperties(\n          fillColor: context.colors.surfaceContainerHigh.toHexStringRGB(),\n          fillOpacity: 1,\n        ),\n      );\n\n      // Reset town fill colors to default\n      await controller.setLayerProperties(\n        BaseMapLayerIds.exptechTownFill,\n        FillLayerProperties(\n          fillColor: context.colors.surfaceContainerHigh.toHexStringRGB(),\n          fillOpacity: 1,\n        ),\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager._clearEew', e, s);\n    } finally {\n      _isUpdatingEew = false;\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    try {\n      _blinkTimer?.cancel();\n      _blinkTimer = null;\n\n      await Future.wait([\n        for (final layer in [\n          _rtsLayerId,\n          '$_rtsLayerId-label-id',\n          '$_rtsLayerId-label-loc',\n          '$_rtsLayerId-label-detail-i',\n          '$_rtsLayerId-label-detail-pga',\n          '$_rtsLayerId-label-detail-pgv',\n          _intensityLayerId,\n          _intensity0LayerId,\n          _boxLayerId,\n          _epicenterLayerId,\n          _pWaveLayerId,\n          _sWaveLayerId,\n        ])\n          controller.setLayerVisibility(layer, false),\n      ]);\n\n      visible = false;\n      _stopFocusTimer();\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    try {\n      _setupBlinkTimer();\n      final hasBox = GlobalProviders.data.rts?.box.isNotEmpty ?? false;\n\n      unawaited(GlobalProviders.data.fetchRtsImmediately());\n\n      await Future.wait([\n        controller.setLayerVisibility(_rtsLayerId, !hasBox),\n        controller.setLayerVisibility('$_rtsLayerId-label-id', !hasBox),\n        controller.setLayerVisibility('$_rtsLayerId-label-loc', !hasBox),\n        controller.setLayerVisibility('$_rtsLayerId-label-detail-i', !hasBox),\n        controller.setLayerVisibility('$_rtsLayerId-label-detail-pga', !hasBox),\n        controller.setLayerVisibility('$_rtsLayerId-label-detail-pgv', !hasBox),\n        controller.setLayerVisibility(_intensityLayerId, hasBox),\n        controller.setLayerVisibility(_intensity0LayerId, hasBox),\n        controller.setLayerVisibility(_boxLayerId, hasBox),\n        ...[\n          for (final layer in [_epicenterLayerId, _pWaveLayerId, _sWaveLayerId])\n            controller.setLayerVisibility(layer, true),\n        ],\n        _focusReset(),\n      ]);\n\n      visible = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    final rtsSourceId = MapSourceIds.rts();\n    final rtsLayerId = MapLayerIds.rts();\n    final intensitySourceId = MapSourceIds.intensity();\n    final intensityLayerId = MapLayerIds.intensity();\n    final intensity0SourceId = MapSourceIds.intensity0();\n    final intensity0LayerId = MapLayerIds.intensity0();\n    final boxSourceId = MapSourceIds.box();\n    final boxLayerId = MapLayerIds.box();\n\n    final eewSourceId = MapSourceIds.eew();\n    final epicenterLayerId = MapLayerIds.eew('x');\n    final pWaveLayerId = MapLayerIds.eew('p');\n    final sWaveLayerId = MapLayerIds.eew('s');\n\n    try {\n      // rts - remove layers/sources in parallel to reduce round-trips\n      await Future.wait([\n        controller.removeLayer(rtsLayerId),\n        controller.removeLayer('$rtsLayerId-label-id'),\n        controller.removeLayer('$rtsLayerId-label-loc'),\n        controller.removeLayer('$rtsLayerId-label-detail-i'),\n        controller.removeLayer('$rtsLayerId-label-detail-pga'),\n        controller.removeLayer('$rtsLayerId-label-detail-pgv'),\n        controller.removeSource(rtsSourceId),\n      ]);\n\n      // intensity\n      await controller.removeLayer(intensityLayerId);\n      TalkerManager.instance.info('Removed Layer \"$intensityLayerId\"');\n      await controller.removeSource(intensitySourceId);\n      TalkerManager.instance.info('Removed Source \"$intensitySourceId\"');\n\n      // intensity0\n      await controller.removeLayer(intensity0LayerId);\n      TalkerManager.instance.info('Removed Layer \"$intensity0LayerId\"');\n      await controller.removeSource(intensity0SourceId);\n      TalkerManager.instance.info('Removed Source \"$intensity0SourceId\"');\n\n      // box\n      await controller.removeLayer(boxLayerId);\n      TalkerManager.instance.info('Removed Layer \"$boxLayerId\"');\n      await controller.removeSource(boxSourceId);\n      TalkerManager.instance.info('Removed Source \"$boxSourceId\"');\n\n      // eew\n      await controller.removeLayer(epicenterLayerId);\n      TalkerManager.instance.info('Removed Layer \"$epicenterLayerId\"');\n      await controller.removeLayer(pWaveLayerId);\n      TalkerManager.instance.info('Removed Layer \"$pWaveLayerId\"');\n      await controller.removeLayer(sWaveLayerId);\n      TalkerManager.instance.info('Removed Layer \"$sWaveLayerId\"');\n      await controller.removeSource(eewSourceId);\n      TalkerManager.instance.info('Removed Source \"$eewSourceId\"');\n    } catch (e, s) {\n      TalkerManager.instance.error('MonitorMapLayerManager.remove', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  void dispose() {\n    _blinkTimer?.cancel();\n    _blinkTimer = null;\n    _stopFocusTimer();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      GlobalProviders.data.setReplayMode(false);\n    });\n    GlobalProviders.data.removeListener(_onDataChanged);\n    currentRtsTime.dispose();\n    displayTimeNotifier.dispose();\n    pingNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => MonitorMapLayerSheet(manager: this);\n}\n\n/// The overlay sheet displayed while the seismic monitor layer is active.\nclass MonitorMapLayerSheet extends StatefulWidget {\n  /// The [MonitorMapLayerManager] whose state drives this sheet.\n  final MonitorMapLayerManager manager;\n\n  /// Creates a [MonitorMapLayerSheet] for the given [manager].\n  const MonitorMapLayerSheet({super.key, required this.manager});\n\n  @override\n  State<MonitorMapLayerSheet> createState() => _MonitorMapLayerSheetState();\n}\n\nclass _MonitorMapLayerSheetState extends State<MonitorMapLayerSheet> {\n  late int localIntensity;\n  late int localArrivalTime;\n  Timer? _timer;\n  int countdown = 0;\n\n  bool _isCollapsed = true;\n\n  void _toggleCollapse() {\n    setState(() => _isCollapsed = !_isCollapsed);\n  }\n\n  void _updateCountdown() {\n    final remainingSeconds = ((localArrivalTime - GlobalProviders.data.currentTime) / 1000).floor();\n    if (remainingSeconds < -1) return;\n\n    setState(() => countdown = remainingSeconds);\n  }\n\n  // Build common alert badge with count indicator\n  Widget _buildAlertBadge(\n    int eewCount, {\n    double iconSize = 16,\n    bool showLabel = false,\n  }) {\n    final colors = context.colors;\n    final theme = context.texts;\n\n    return Container(\n      decoration: BoxDecoration(\n        color: colors.error,\n        borderRadius: .circular(8),\n      ),\n      padding: eewCount > 1 || showLabel\n          ? const .fromLTRB(8, 6, 12, 6)\n          : const .fromLTRB(8, 6, 8, 6),\n      child: Row(\n        mainAxisSize: .min,\n        spacing: 4,\n        children: [\n          Icon(\n            Symbols.crisis_alert_rounded,\n            color: colors.onError,\n            weight: 700,\n            size: iconSize,\n          ),\n          if (showLabel)\n            Text(\n              'EEW'.i18n,\n              style: theme.labelLarge!.copyWith(\n                color: colors.onError,\n                fontWeight: .bold,\n              ),\n            )\n          else if (eewCount > 1)\n            RichText(\n              text: TextSpan(\n                children: [\n                  TextSpan(\n                    text: '1',\n                    style: theme.labelMedium!.copyWith(\n                      color: colors.onError,\n                      fontWeight: .bold,\n                    ),\n                  ),\n                  TextSpan(\n                    text: '/$eewCount',\n                    style: theme.labelMedium!.copyWith(\n                      color: colors.onError.withValues(alpha: 0.6),\n                      fontWeight: .bold,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n\n  // Build collapsed or expanded EEW info\n  Widget _buildEewContent(Eew data, int eewCount, bool hasLocation) {\n    final colors = context.colors;\n    final theme = context.texts;\n\n    if (_isCollapsed) {\n      // Collapsed view - compact info\n      return Column(\n        crossAxisAlignment: .start,\n        children: [\n          Row(\n            mainAxisAlignment: .spaceBetween,\n            children: [\n              Row(\n                spacing: 8,\n                children: [\n                  _buildAlertBadge(eewCount),\n                  Text(\n                    '#${data.serial} ${data.info.time.toSimpleDateTimeString()} ${data.info.location}',\n                    style: theme.bodyMedium!.copyWith(\n                      fontWeight: .bold,\n                      color: colors.onErrorContainer,\n                    ),\n                  ),\n                ],\n              ),\n              Icon(\n                Symbols.expand_less_rounded,\n                color: colors.onErrorContainer,\n                size: 24,\n              ),\n            ],\n          ),\n          Padding(\n            padding: const .only(top: 8),\n            child: hasLocation\n                ? Row(\n                    mainAxisAlignment: .spaceBetween,\n                    children: [\n                      StyledText(\n                        text: '規模 <bold>M{magnitude}</bold>，所在地預估<bold>{intensity}</bold>'.i18n\n                            .args({\n                              'magnitude': data.info.magnitude.toStringAsFixed(1),\n                              'intensity': localIntensity.asIntensityLabel,\n                            }),\n                        style: theme.bodyMedium!.copyWith(\n                          color: colors.onErrorContainer,\n                        ),\n                        tags: {\n                          'bold': StyledTextTag(\n                            style: const TextStyle(fontWeight: .bold),\n                          ),\n                        },\n                      ),\n                      Text(\n                        countdown > 0\n                            ? '{countdown}秒後抵達'.i18n.args({\n                                'countdown': countdown,\n                              })\n                            : '已抵達'.i18n,\n                        style: theme.bodyMedium!.copyWith(\n                          fontWeight: .bold,\n                          color: colors.onErrorContainer,\n                          height: 1,\n                          leadingDistribution: TextLeadingDistribution.even,\n                        ),\n                      ),\n                    ],\n                  )\n                : StyledText(\n                    text: '規模 <bold>M{magnitude}</bold>，深度<bold>{depth}</bold>公里'.i18n.args({\n                      'magnitude': data.info.magnitude.toStringAsFixed(\n                        1,\n                      ),\n                      'depth': data.info.depth.toStringAsFixed(1),\n                    }),\n                    style: theme.bodyMedium!.copyWith(\n                      color: colors.onErrorContainer,\n                    ),\n                    tags: {\n                      'bold': StyledTextTag(\n                        style: const TextStyle(fontWeight: .bold),\n                      ),\n                    },\n                  ),\n          ),\n        ],\n      );\n    } else {\n      // Expanded view - detailed info\n      return Column(\n        mainAxisSize: .min,\n        children: [\n          Row(\n            mainAxisAlignment: .spaceBetween,\n            children: [\n              Row(\n                spacing: 8,\n                children: [\n                  _buildAlertBadge(eewCount, iconSize: 22, showLabel: true),\n                  Text(\n                    '第 {serial} 報'.i18n.args({'serial': data.serial}),\n                    style: theme.bodyLarge!.copyWith(\n                      color: colors.onErrorContainer,\n                    ),\n                  ),\n                ],\n              ),\n              Icon(\n                Symbols.expand_more_rounded,\n                color: colors.onErrorContainer,\n                size: 24,\n              ),\n            ],\n          ),\n          Padding(\n            padding: const .only(top: 8),\n            child: StyledText(\n              text: hasLocation\n                  ? '{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、所在地最大震度<bold>{intensity}</bold>。'\n                        .i18n\n                        .args({\n                          'time': data.info.time.toSimpleDateTimeString(),\n                          'location': data.info.location,\n                          'magnitude': data.info.magnitude.toStringAsFixed(1),\n                          'intensity': localIntensity.asIntensityLabel,\n                        })\n                  : '{time} 左右，<bold>{location}</bold>附近發生有感地震，預估規模 <bold>M{magnitude}</bold>、深度<bold>{depth}</bold>公里。'\n                        .i18n\n                        .args({\n                          'time': data.info.time.toSimpleDateTimeString(),\n                          'location': data.info.location,\n                          'magnitude': data.info.magnitude.toStringAsFixed(1),\n                          'depth': data.info.depth.toStringAsFixed(1),\n                        }),\n              style: theme.bodyLarge!.copyWith(color: colors.onErrorContainer),\n              tags: {\n                'bold': StyledTextTag(\n                  style: const TextStyle(fontWeight: .bold),\n                ),\n              },\n            ),\n          ),\n          if (hasLocation) _buildLocationDetails(),\n        ],\n      );\n    }\n  }\n\n  // Build location-specific details (intensity and countdown)\n  Widget _buildLocationDetails() {\n    return Selector<SettingsLocationModel, String?>(\n      selector: (context, model) => model.code,\n      builder: (context, code, child) {\n        if (code == null) return const SizedBox.shrink();\n\n        final colors = context.colors;\n        final theme = context.texts;\n\n        return Padding(\n          padding: const .only(top: 8, bottom: 4),\n          child: IntrinsicHeight(\n            child: Row(\n              crossAxisAlignment: .start,\n              children: [\n                Expanded(\n                  child: Padding(\n                    padding: const .all(4),\n                    child: Column(\n                      mainAxisSize: .min,\n                      crossAxisAlignment: .stretch,\n                      children: [\n                        Text(\n                          '所在地預估'.i18n,\n                          style: theme.labelLarge!.copyWith(\n                            color: colors.onErrorContainer.withValues(\n                              alpha: 0.6,\n                            ),\n                          ),\n                        ),\n                        Padding(\n                          padding: const .only(top: 12, bottom: 8),\n                          child: Text(\n                            localIntensity.asIntensityLabel,\n                            style: theme.displayMedium!.copyWith(\n                              fontWeight: .bold,\n                              color: colors.onErrorContainer,\n                              height: 1,\n                              leadingDistribution: TextLeadingDistribution.even,\n                            ),\n                            textAlign: .center,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n                VerticalDivider(\n                  color: colors.onErrorContainer.withValues(alpha: 0.4),\n                  width: 24,\n                ),\n                Expanded(\n                  child: Padding(\n                    padding: const .all(4),\n                    child: Column(\n                      mainAxisSize: .min,\n                      crossAxisAlignment: .stretch,\n                      children: [\n                        Text(\n                          '震波'.i18n,\n                          style: theme.labelLarge!.copyWith(\n                            color: colors.onErrorContainer.withValues(\n                              alpha: 0.6,\n                            ),\n                          ),\n                        ),\n                        Padding(\n                          padding: const .only(top: 12, bottom: 8),\n                          child: countdown > 0\n                              ? RichText(\n                                  text: TextSpan(\n                                    children: [\n                                      TextSpan(\n                                        text: countdown.toString(),\n                                        style: TextStyle(\n                                          fontSize: theme.displayMedium!.fontSize! * 1.15,\n                                        ),\n                                      ),\n                                      TextSpan(\n                                        text: ' 秒'.i18n,\n                                        style: TextStyle(\n                                          fontSize: theme.labelLarge!.fontSize,\n                                        ),\n                                      ),\n                                    ],\n                                    style: theme.displayMedium!.copyWith(\n                                      fontWeight: .bold,\n                                      color: colors.onErrorContainer,\n                                      height: 1,\n                                      leadingDistribution: TextLeadingDistribution.even,\n                                    ),\n                                  ),\n                                  textAlign: .center,\n                                )\n                              : Text(\n                                  '抵達'.i18n,\n                                  style: theme.displayMedium!.copyWith(\n                                    fontSize: theme.displayMedium!.fontSize! * 0.81,\n                                    fontWeight: .bold,\n                                    color: colors.onErrorContainer,\n                                    height: 1,\n                                    leadingDistribution: TextLeadingDistribution.even,\n                                  ),\n                                  textAlign: .center,\n                                ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Selector<DpipDataModel, UnmodifiableListView<Eew>>(\n      selector: (_, data) => data.eew,\n      builder: (context, activeEew, child) {\n        return Stack(\n          children: [\n            ResponsiveContainer(\n              maxWidth: 720,\n              child: MorphingSheet(\n                title: '強震監視器'.i18n,\n                borderRadius: .circular(16),\n                elevation: 4,\n                borderWidth: activeEew.isNotEmpty ? 2 : null,\n                borderColor: activeEew.isNotEmpty ? context.colors.error : null,\n                backgroundColor: activeEew.isNotEmpty ? context.colors.errorContainer : null,\n                partialBuilder: (context, controller, sheetController) {\n                  if (activeEew.isEmpty) {\n                    return Padding(\n                      padding: const .all(12),\n                      child: Text('目前沒有生效中的地震速報'.i18n),\n                    );\n                  }\n\n                  final data = activeEew.first;\n                  final hasLocation = GlobalProviders.location.coordinates != null;\n\n                  // Calculate location-specific info if available\n                  if (hasLocation) {\n                    final info = eewLocationInfo(\n                      data.info.magnitude,\n                      data.info.depth,\n                      data.info.latitude,\n                      data.info.longitude,\n                      GlobalProviders.location.coordinates!.latitude,\n                      GlobalProviders.location.coordinates!.longitude,\n                    );\n\n                    localIntensity = intensityFloatToInt(info.i);\n                    localArrivalTime =\n                        (data.info.time + sWaveTimeByDistance(data.info.depth, info.dist)).floor();\n\n                    WidgetsBinding.instance.addPostFrameCallback(\n                      (_) => _updateCountdown(),\n                    );\n                    _timer ??= Timer.periodic(\n                      const Duration(seconds: 1),\n                      (_) => _updateCountdown(),\n                    );\n                  }\n\n                  return InkWell(\n                    onTap: _toggleCollapse,\n                    child: Padding(\n                      padding: const .all(12),\n                      child: _buildEewContent(\n                        data,\n                        activeEew.length,\n                        hasLocation,\n                      ),\n                    ),\n                  );\n                },\n              ),\n            ),\n            // Intensity legend - positioned at top left\n            // Show RTS mode when no EEW, show EEW mode during EEW\n            Positioned(\n              top: 24 + 48 + 16,\n              left: 24,\n              child: SafeArea(\n                child: BlurredContainer(\n                  elevation: 4,\n                  shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n                  child: IntensityLegend(\n                    mode: activeEew.isNotEmpty ? IntensityLegendMode.eew : IntensityLegendMode.rts,\n                  ),\n                ),\n              ),\n            ),\n            Positioned(\n              top: 26,\n              left: 95,\n              right: 95,\n              child: SafeArea(\n                child: Align(\n                  alignment: .topCenter,\n                  child: ValueListenableBuilder<String>(\n                    valueListenable: widget.manager.displayTimeNotifier,\n                    builder: (context, displayTime, child) {\n                      final isStale = displayTime.endsWith('|STALE');\n                      final timeText = isStale ? displayTime.replaceAll('|STALE', '') : displayTime;\n\n                      return ValueListenableBuilder<double>(\n                        valueListenable: widget.manager.pingNotifier,\n                        builder: (context, ping, child) {\n                          final isDataOk = widget.manager.dataStatus;\n                          final pingText = (!isDataOk) ? 'N/A' : '${ping.toStringAsFixed(0)}ms';\n                          final pingColor = (!isDataOk)\n                              ? Colors.red\n                              : (ping > 5000)\n                              ? Colors.red\n                              : (ping > 1000)\n                              ? Colors.orange\n                              : Colors.green;\n\n                          return Container(\n                            padding: const .all(8),\n                            decoration: BoxDecoration(\n                              color: context.colors.surface.withValues(\n                                alpha: 0.5,\n                              ),\n                              borderRadius: .circular(16),\n                            ),\n                            child: Row(\n                              mainAxisSize: .min,\n                              mainAxisAlignment: .center,\n                              children: [\n                                Text(\n                                  timeText,\n                                  textAlign: .center,\n                                  style: TextStyle(\n                                    color: widget.manager.isReplayMode\n                                        ? Colors.orange\n                                        : (isStale ? Colors.red : context.colors.onSurface),\n                                    fontSize: 16,\n                                  ),\n                                ),\n                                const SizedBox(width: 4),\n                                SizedBox(\n                                  width: 55,\n                                  child: Text(\n                                    pingText,\n                                    textAlign: .right,\n                                    style: TextStyle(\n                                      fontSize: 12,\n                                      fontWeight: .bold,\n                                      color: pingColor,\n                                    ),\n                                  ),\n                                ),\n                              ],\n                            ),\n                          );\n                        },\n                      );\n                    },\n                  ),\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _timer?.cancel();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/precipitation.dart",
    "content": "/// Map layer manager and associated UI for precipitation data.\nlibrary;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/rain.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// Intermediate data object for a single rain-gauge station reading.\nclass RainData {\n  /// The station's latitude in decimal degrees.\n  final double latitude;\n\n  /// The station's longitude in decimal degrees.\n  final double longitude;\n\n  /// The measured rainfall amount in millimetres.\n  final double rainfall;\n\n  /// The human-readable name of the weather station.\n  final String stationName;\n\n  /// The county in which the station is located.\n  final String county;\n\n  /// The town in which the station is located.\n  final String town;\n\n  /// The station's unique identifier.\n  final String id;\n\n  /// Creates a [RainData] with all required fields.\n  RainData({\n    required this.latitude,\n    required this.longitude,\n    required this.rainfall,\n    required this.stationName,\n    required this.county,\n    required this.town,\n    required this.id,\n  });\n}\n\n/// Manages the precipitation overlay layer on the DPIP map.\nclass PrecipitationMapLayerManager extends MapLayerManager {\n  /// Creates a [PrecipitationMapLayerManager] bound to [context] and\n  /// [controller].\n  PrecipitationMapLayerManager(super.context, super.controller);\n\n  /// The available accumulation intervals selectable by the user.\n  static const precipitationIntervals = [\n    'now',\n    '10m',\n    '1h',\n    '3h',\n    '6h',\n    '12h',\n    '24h',\n    '2d',\n    '3d',\n  ];\n\n  /// Vertical offset of the first text label line above the station circle.\n  static const double kLabelBaseOffset = 1.0;\n\n  /// Vertical spacing between consecutive text label lines.\n  static const double kLabelLineHeight = 1.1;\n\n  /// The currently displayed precipitation observation time string.\n  final currentPrecipitationTime = ValueNotifier<String?>(\n    GlobalProviders.data.precipitation.firstOrNull,\n  );\n\n  /// The currently selected accumulation interval key.\n  final currentPrecipitationInterval = ValueNotifier<String>('now');\n\n  /// Whether a time-change or interval-change operation is in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  DateTime? _lastFetchTime;\n\n  /// Called with the new time string whenever the displayed time changes.\n  Function(String)? onTimeChanged;\n\n  /// Switches the displayed precipitation data to [time].\n  ///\n  /// Does nothing if [time] is already current or a load is in progress.\n  Future<void> setPrecipitationTime(String time) async {\n    if (currentPrecipitationTime.value == time || isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      await remove();\n      currentPrecipitationTime.value = time;\n      await setup();\n\n      onTimeChanged?.call(time);\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'PrecipitationMapLayerManager.setPrecipitationTime',\n        e,\n        s,\n      );\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  /// Switches the visible accumulation interval to [interval].\n  ///\n  /// Toggles layer visibility without reloading source data.\n  Future<void> setPrecipitationInterval(String interval) async {\n    if (currentPrecipitationInterval.value == interval) return;\n\n    try {\n      final layerId = MapLayerIds.precipitation(currentPrecipitationTime.value);\n      final showLayerId = '$layerId-$interval';\n      final hideLayerId = '$layerId-${currentPrecipitationInterval.value}';\n\n      await controller.setLayerVisibility(showLayerId, true);\n      await controller.setLayerVisibility('$showLayerId-label-name', true);\n      await controller.setLayerVisibility('$showLayerId-label-value', true);\n\n      await controller.setLayerVisibility(hideLayerId, false);\n      await controller.setLayerVisibility('$hideLayerId-label-name', false);\n      await controller.setLayerVisibility('$hideLayerId-label-value', false);\n\n      currentPrecipitationInterval.value = interval;\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'PrecipitationMapLayerManager.setPrecipitationInterval',\n        e,\n        s,\n      );\n    }\n  }\n\n  Future<void> _focus() async {\n    try {\n      final location = GlobalProviders.location.coordinates;\n\n      if (location != null && location.isValid) {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(location, 7.4),\n        );\n      } else {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n        );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('PrecipitationMapLayerManager._focus', e, s);\n    }\n  }\n\n  Future<void> _fetchData() async {\n    try {\n      final precipitationList = (await ExpTech().getRainList()).reversed.toList();\n      if (!context.mounted) return;\n\n      GlobalProviders.data.setPrecipitation(precipitationList);\n      currentPrecipitationTime.value ??= precipitationList.first;\n      _lastFetchTime = DateTime.now();\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'PrecipitationMapLayerManager._fetchData',\n        e,\n        s,\n      );\n    }\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    final colors = context.colors;\n\n    try {\n      if (GlobalProviders.data.precipitation.isEmpty) {\n        await _fetchData();\n      }\n\n      final time = currentPrecipitationTime.value;\n\n      if (time == null) throw Exception('Time is null');\n\n      final sourceId = MapSourceIds.precipitation(time);\n      final layerId = MapLayerIds.precipitation(time);\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (!isSourceExists) {\n        late final List<RainStation> rainData;\n\n        if (GlobalProviders.data.rainData.containsKey(time)) {\n          rainData = GlobalProviders.data.rainData[time]!;\n        } else {\n          rainData = await ExpTech().getRain(time);\n          GlobalProviders.data.setRainData(time, rainData);\n        }\n\n        final features = rainData.map((station) => station.toFeatureBuilder());\n\n        final data = GeoJsonBuilder().setFeatures(features).build();\n\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        final Map<String, LayerProperties> properties = {\n          for (final interval in precipitationIntervals)\n            ...({\n              interval: CircleLayerProperties(\n                circleColor: [\n                  Expressions.interpolate,\n                  ['linear'],\n                  [Expressions.get, interval],\n                  0,\n                  '#c2c2c2',\n                  10,\n                  '#9cfcff',\n                  30,\n                  '#059bff',\n                  50,\n                  '#39ff03',\n                  100,\n                  '#fffb03',\n                  200,\n                  '#ff9500',\n                  300,\n                  '#ff0000',\n                  500,\n                  '#fb00ff',\n                  1000,\n                  '#960099',\n                  2000,\n                  '#000000',\n                ],\n                circleRadius: kCircleIconSize,\n                circleOpacity: 0.75,\n                circleStrokeColor: colors.outlineVariant.toHexStringRGB(),\n                circleStrokeWidth: 0.5,\n                circleStrokeOpacity: 0.75,\n                visibility: interval == currentPrecipitationInterval.value ? 'visible' : 'none',\n              ),\n              '$interval-label-name': SymbolLayerProperties(\n                textField: [Expressions.get, 'name'],\n                textSize: 10,\n                textColor: colors.onSurfaceVariant.toHexStringRGB(),\n                textHaloColor: colors.outlineVariant.toHexStringRGB(),\n                textHaloWidth: 1,\n                textFont: ['Noto Sans TC Bold'],\n                textOffset: [0, kLabelBaseOffset],\n                textAnchor: 'top',\n                textAllowOverlap: true,\n                textIgnorePlacement: true,\n                visibility: interval == currentPrecipitationInterval.value ? 'visible' : 'none',\n              ),\n              '$interval-label-value': SymbolLayerProperties(\n                textField: [\n                  Expressions.concat,\n                  [Expressions.get, interval],\n                  'mm',\n                ],\n                textSize: 10,\n                textColor: colors.onSurfaceVariant.toHexStringRGB(),\n                textHaloColor: colors.outlineVariant.toHexStringRGB(),\n                textHaloWidth: 1,\n                textFont: ['Noto Sans TC Bold'],\n                textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 1],\n                textAnchor: 'top',\n                textAllowOverlap: true,\n                textIgnorePlacement: true,\n                visibility: interval == currentPrecipitationInterval.value ? 'visible' : 'none',\n              ),\n            }),\n        };\n\n        await Future.wait(\n          properties.entries.map((entry) {\n            // Detect label entries more precisely using '-label-' marker\n            final isValueLayer = entry.key.contains('-label-');\n            final interval = isValueLayer ? entry.key.split('-label-')[0] : entry.key;\n\n            return controller.addLayer(\n              sourceId,\n              '$layerId-${entry.key}',\n              entry.value,\n              belowLayerId: BaseMapLayerIds.userLocation,\n              minzoom: isValueLayer ? 10 : null,\n              filter: [\n                Expressions.larger,\n                [Expressions.get, interval],\n                0,\n              ],\n            );\n          }),\n        );\n      }\n\n      if (isSourceExists && isLayerExists) return;\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('PrecipitationMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    final layerId = MapLayerIds.precipitation(currentPrecipitationTime.value);\n    final hideLayerId = '$layerId-${currentPrecipitationInterval.value}';\n    final hideNameLayerId = '$layerId-${currentPrecipitationInterval.value}-label-name';\n    final hideValueLayerId = '$layerId-${currentPrecipitationInterval.value}-label-value';\n\n    try {\n      await controller.setLayerVisibility(hideLayerId, false);\n      await controller.setLayerVisibility(hideNameLayerId, false);\n      await controller.setLayerVisibility(hideValueLayerId, false);\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('PrecipitationMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final layerId = MapLayerIds.precipitation(currentPrecipitationTime.value);\n    final showLayerId = '$layerId-${currentPrecipitationInterval.value}';\n    final showNameLayerId = '$layerId-${currentPrecipitationInterval.value}-label-name';\n    final showValueLayerId = '$layerId-${currentPrecipitationInterval.value}-label-value';\n\n    try {\n      await controller.setLayerVisibility(showLayerId, true);\n      await controller.setLayerVisibility(showNameLayerId, true);\n      await controller.setLayerVisibility(showValueLayerId, true);\n\n      await _focus();\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5)\n        await _fetchData();\n    } catch (e, s) {\n      TalkerManager.instance.error('PrecipitationMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final layerId = MapLayerIds.precipitation(currentPrecipitationTime.value);\n      final sourceId = MapSourceIds.precipitation(\n        currentPrecipitationTime.value,\n      );\n\n      final removals = <Future<void>>[];\n      for (final interval in precipitationIntervals) {\n        removals.add(controller.removeLayer('$layerId-$interval'));\n        removals.add(controller.removeLayer('$layerId-$interval-label-name'));\n        removals.add(controller.removeLayer('$layerId-$interval-label-value'));\n      }\n      removals.add(controller.removeSource(sourceId));\n      await Future.wait(removals);\n    } catch (e, s) {\n      TalkerManager.instance.error('PrecipitationMapLayerManager.remove', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  Widget build(BuildContext context) => PrecipitationMapLayerSheet(manager: this);\n}\n\n/// The bottom sheet and legend overlay for the precipitation layer.\nclass PrecipitationMapLayerSheet extends StatelessWidget {\n  /// The [PrecipitationMapLayerManager] whose state drives this sheet.\n  final PrecipitationMapLayerManager manager;\n\n  /// Creates a [PrecipitationMapLayerSheet] for the given [manager].\n  const PrecipitationMapLayerSheet({super.key, required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    String getIntervalLabel(String interval) => switch (interval) {\n      'now' => '今日'.i18n,\n      '10m' => '10 分鐘'.i18n,\n      '1h' => '1 小時'.i18n,\n      '3h' => '3 小時'.i18n,\n      '6h' => '6 小時'.i18n,\n      '12h' => '12 小時'.i18n,\n      '24h' => '24 小時'.i18n,\n      '2d' => '2 天'.i18n,\n      '3d' => '3 天'.i18n,\n      _ => interval,\n    };\n\n    return Stack(\n      children: [\n        MorphingSheet(\n          title: '降水'.i18n,\n          borderRadius: .circular(16),\n          elevation: 4,\n          partialBuilder: (context, controller, sheetController) {\n            return Padding(\n              padding: const .symmetric(vertical: 8),\n              child: Selector<DpipDataModel, UnmodifiableListView<String>>(\n                selector: (context, model) => model.precipitation,\n                builder: (context, precipitation, header) {\n                  final times = precipitation.map((time) {\n                    final t = time.toSimpleDateTimeString().split(' ');\n                    return (date: t[0], time: t[1], value: time);\n                  });\n                  final grouped = times.groupListsBy((time) => time.date).entries.toList();\n\n                  return Column(\n                    mainAxisSize: .min,\n                    children: [\n                      header!,\n                      SizedBox(\n                        height: kMinInteractiveDimension,\n                        child: ValueListenableBuilder<String?>(\n                          valueListenable: manager.currentPrecipitationInterval,\n                          builder: (context, currentPrecipitationInterval, child) {\n                            const intervals = PrecipitationMapLayerManager.precipitationIntervals;\n\n                            return ListView.separated(\n                              padding: const .symmetric(\n                                horizontal: 16,\n                              ),\n                              scrollDirection: .horizontal,\n                              physics: const AlwaysScrollableScrollPhysics(),\n                              itemCount: intervals.length,\n                              itemBuilder: (context, index) {\n                                final interval = intervals[index];\n                                final isSelected = interval == currentPrecipitationInterval;\n\n                                return ValueListenableBuilder<bool>(\n                                  valueListenable: manager.isLoading,\n                                  builder: (context, isLoading, child) {\n                                    return FilterChip(\n                                      selected: isSelected,\n                                      showCheckmark: !isLoading,\n                                      label: Text(\n                                        getIntervalLabel(interval),\n                                      ),\n                                      side: BorderSide(\n                                        color: isSelected\n                                            ? context.colors.primary\n                                            : context.colors.outlineVariant,\n                                      ),\n                                      avatar: isSelected && isLoading ? const LoadingIcon() : null,\n                                      onSelected: isLoading\n                                          ? null\n                                          : (selected) {\n                                              if (!selected) return;\n                                              manager.setPrecipitationInterval(\n                                                interval,\n                                              );\n                                            },\n                                    );\n                                  },\n                                );\n                              },\n                              separatorBuilder: (context, index) => const SizedBox(width: 8),\n                            );\n                          },\n                        ),\n                      ),\n                      SizedBox(\n                        height: kMinInteractiveDimension,\n                        child: ValueListenableBuilder<String?>(\n                          valueListenable: manager.currentPrecipitationTime,\n                          builder: (context, currentPrecipitationTime, child) {\n                            return ListView.builder(\n                              padding: const .symmetric(\n                                horizontal: 16,\n                              ),\n                              scrollDirection: .horizontal,\n                              physics: const AlwaysScrollableScrollPhysics(),\n                              itemCount: grouped.length,\n                              itemBuilder: (context, index) {\n                                final MapEntry(key: date, value: group) = grouped[index];\n\n                                final children = <Widget>[Text(date)];\n\n                                for (final time in group) {\n                                  final isSelected = time.value == currentPrecipitationTime;\n\n                                  children.add(\n                                    ValueListenableBuilder<bool>(\n                                      valueListenable: manager.isLoading,\n                                      builder: (context, isLoading, child) {\n                                        return FilterChip(\n                                          selected: isSelected,\n                                          showCheckmark: !isLoading,\n                                          label: Text(time.time),\n                                          side: BorderSide(\n                                            color: isSelected\n                                                ? context.colors.primary\n                                                : context.colors.outlineVariant,\n                                          ),\n                                          avatar: isSelected && isLoading\n                                              ? const LoadingIcon()\n                                              : null,\n                                          onSelected: isLoading\n                                              ? null\n                                              : (selected) {\n                                                  if (!selected) return;\n                                                  manager.setPrecipitationTime(\n                                                    time.value,\n                                                  );\n                                                },\n                                        );\n                                      },\n                                    ),\n                                  );\n                                }\n\n                                children.add(\n                                  const Padding(\n                                    padding: .only(right: 8),\n                                    child: VerticalDivider(\n                                      width: 16,\n                                      indent: 8,\n                                      endIndent: 8,\n                                    ),\n                                  ),\n                                );\n\n                                return Row(\n                                  mainAxisSize: .min,\n                                  spacing: 8,\n                                  children: children,\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ),\n                    ],\n                  );\n                },\n                child: Padding(\n                  padding: const .symmetric(\n                    horizontal: 16,\n                    vertical: 8,\n                  ),\n                  child: Row(\n                    spacing: 8,\n                    children: [\n                      const Icon(Symbols.water_drop_rounded, size: 24),\n                      Text('降水'.i18n, style: context.texts.titleMedium),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n        Positioned(\n          top: 24 + 48 + 16,\n          left: 24,\n          child: SafeArea(\n            child: BlurredContainer(\n              elevation: 4,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n              child: ColorLegend(\n                unit: 'mm',\n                items: [\n                  ColorLegendItem(color: const Color(0xFF000000), value: 2000),\n                  ColorLegendItem(color: const Color(0xFF960099), value: 1000),\n                  ColorLegendItem(color: const Color(0xFFFB00FF), value: 500),\n                  ColorLegendItem(color: const Color(0xFFFF0000), value: 300),\n                  ColorLegendItem(color: const Color(0xFFFF9500), value: 200),\n                  ColorLegendItem(color: const Color(0xFFFFFB03), value: 100),\n                  ColorLegendItem(color: const Color(0xFF39FF03), value: 50),\n                  ColorLegendItem(color: const Color(0xFF059BFF), value: 30),\n                  ColorLegendItem(color: const Color(0xFF9CFCFF), value: 10),\n                  ColorLegendItem(color: const Color(0xffc2c2c2), value: 0),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/radar.dart",
    "content": "/// Map layer manager and associated UI for radar echo data.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/route.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// Manages the radar-echo overlay layer and its auto-play functionality.\nclass RadarMapLayerManager extends MapLayerManager {\n  /// Creates a [RadarMapLayerManager].\n  ///\n  /// [getActiveLayerCount] is queried to determine whether this manager is in\n  /// multi-layer mode, which disables auto-play.\n  RadarMapLayerManager(\n    super.context,\n    super.controller, {\n    this.getActiveLayerCount,\n  });\n\n  /// The currently displayed radar observation time string.\n  final currentRadarTime = ValueNotifier<String?>(\n    GlobalProviders.data.radar.firstOrNull,\n  );\n\n  /// Whether a time-change operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  /// Whether the auto-play animation is currently running.\n  final isPlaying = ValueNotifier<bool>(false);\n\n  /// The start time of the auto-play range, or `null` when not set.\n  final playStartTime = ValueNotifier<String?>(null);\n\n  /// The end time of the auto-play range, or `null` when not set.\n  final playEndTime = ValueNotifier<String?>(null);\n\n  DateTime? _lastFetchTime;\n\n  Timer? _playTimer;\n  final Set<String> _preloadedLayers = {};\n\n  /// Returns the number of currently active map layers.\n  ///\n  /// Used to detect multi-layer mode and suppress auto-play.\n  final int Function()? getActiveLayerCount;\n\n  /// Moves the displayed radar frame to [time], stopping auto-play if active.\n  ///\n  /// Also adjusts [playStartTime] if necessary and preloads adjacent frames.\n  Future<void> updateRadarTime(String time) async {\n    if (isPlaying.value) {\n      stopAutoPlay();\n      TalkerManager.instance.info('Auto-play stopped due to external control');\n    }\n\n    if (playStartTime.value != null) {\n      final radarList = GlobalProviders.data.radar;\n      final startIndex = radarList.indexOf(playStartTime.value);\n      final newCurrentIndex = radarList.indexOf(time);\n\n      if (startIndex != -1 && newCurrentIndex != -1 && newCurrentIndex > startIndex) {\n        final newStartIndex = newCurrentIndex + 1;\n        if (newStartIndex < radarList.length) {\n          playStartTime.value = radarList[newStartIndex];\n          TalkerManager.instance.info(\n            'Moved start time to right of current time: ${radarList[newStartIndex]}',\n          );\n\n          final nextIndex = newStartIndex + 1;\n          if (nextIndex < radarList.length) {\n            playEndTime.value = radarList[nextIndex];\n            TalkerManager.instance.info(\n              'Set end time to next item: ${playEndTime.value}',\n            );\n          } else {\n            playEndTime.value = null;\n            TalkerManager.instance.info(\n              'Cleared end time because start time is at the end',\n            );\n          }\n        }\n      }\n    }\n\n    await _updateRadarTileUrl(time);\n    await _preloadAdjacentLayers(time);\n  }\n\n  /// Sets [playStartTime] to [time] and adjusts [currentRadarTime] if needed.\n  ///\n  /// The current time is moved to the left of the new start time to keep the\n  /// playback range valid.\n  void setPlayStartTime(String time) {\n    if (time == currentRadarTime.value) {\n      TalkerManager.instance.info('Cannot set current time as play start time');\n      return;\n    }\n\n    final radarList = GlobalProviders.data.radar;\n    final startIndex = radarList.indexOf(time);\n    final currentIndex = currentRadarTime.value != null\n        ? radarList.indexOf(currentRadarTime.value)\n        : -1;\n\n    if (startIndex != -1 && currentIndex != -1 && startIndex < currentIndex) {\n      final newCurrentIndex = startIndex - 1;\n      if (newCurrentIndex >= 0) {\n        updateRadarTime(radarList[newCurrentIndex]);\n        TalkerManager.instance.info(\n          'Moved current time to left of start time: ${radarList[newCurrentIndex]}',\n        );\n      }\n    }\n\n    playStartTime.value = time;\n\n    if (startIndex != -1) {\n      final nextIndex = startIndex + 1;\n      if (nextIndex < radarList.length) {\n        playEndTime.value = radarList[nextIndex];\n        TalkerManager.instance.info(\n          'Set end time to next item: ${playEndTime.value}',\n        );\n      } else {\n        playEndTime.value = null;\n        TalkerManager.instance.info(\n          'Cleared end time because start time is at the end',\n        );\n      }\n    }\n\n    TalkerManager.instance.info('Set play start time to: $time');\n  }\n\n  /// Whether the auto-play start conditions are satisfied.\n  ///\n  /// Returns `true` when no start time is set (play from the list end), or\n  /// when the start time is to the right of the current time.\n  bool get canPlay {\n    if (playStartTime.value == null) return true;\n\n    final radarList = GlobalProviders.data.radar;\n    final startIndex = radarList.indexOf(playStartTime.value);\n    final currentIndex = currentRadarTime.value != null\n        ? radarList.indexOf(currentRadarTime.value)\n        : -1;\n\n    return startIndex != -1 && currentIndex != -1 && startIndex > currentIndex;\n  }\n\n  /// Whether more than one map layer is currently active.\n  ///\n  /// Auto-play is stopped automatically when this returns `true`.\n  bool get isMultiLayerMode {\n    final count = getActiveLayerCount?.call() ?? 1;\n    final isMulti = count > 1;\n\n    if (isMulti && isPlaying.value) {\n      stopAutoPlay();\n    }\n\n    return isMulti;\n  }\n\n  Future<void> _updateRadarTileUrl(String time) async {\n    if (currentRadarTime.value == time || isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      if (currentRadarTime.value != null) {\n        await _hideLayer(currentRadarTime.value!);\n      }\n\n      currentRadarTime.value = time;\n\n      await _setupAndShowLayer(time);\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'RadarMapLayerManager._updateRadarTileUrl',\n        e,\n        s,\n      );\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future<void> _setupAndShowLayer(String time) async {\n    final sourceId = MapSourceIds.radar(time);\n    final layerId = MapLayerIds.radar(time);\n\n    final isSourceExists = (await controller.getSourceIds()).contains(sourceId);\n    final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n    if (!isSourceExists) {\n      final properties = RasterSourceProperties(\n        tiles: [radarTile(time)],\n        tileSize: 256,\n      );\n\n      await controller.addSource(sourceId, properties);\n    }\n\n    if (!isLayerExists) {\n      final properties = RasterLayerProperties(\n        visibility: visible ? 'visible' : 'none',\n      );\n\n      await controller.addLayer(\n        sourceId,\n        layerId,\n        properties,\n        belowLayerId: BaseMapLayerIds.exptechCountyOutline,\n      );\n    } else if (visible) {\n      await controller.setLayerVisibility(layerId, true);\n    }\n\n    _preloadedLayers.add(time);\n  }\n\n  Future<void> _hideLayer(String time) async {\n    final layerId = MapLayerIds.radar(time);\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager._hideLayer', e, s);\n    }\n  }\n\n  Future<void> _preloadAdjacentLayers(String currentTime) async {\n    final radarList = GlobalProviders.data.radar;\n    if (radarList.isEmpty) return;\n\n    final currentIndex = radarList.indexOf(currentTime);\n    if (currentIndex == -1) return;\n\n    final layersToPreload = <String>[];\n\n    for (int i = 1; i <= 3; i++) {\n      if (currentIndex - i >= 0) {\n        layersToPreload.add(radarList[currentIndex - i]);\n      }\n      if (currentIndex + i < radarList.length) {\n        layersToPreload.add(radarList[currentIndex + i]);\n      }\n    }\n\n    for (final time in layersToPreload) {\n      if (!_preloadedLayers.contains(time)) {\n        try {\n          await _setupAndShowLayer(time);\n          await _hideLayer(time);\n        } catch (e, s) {\n          TalkerManager.instance.error(\n            'Failed to preload radar layer: $time',\n            e,\n            s,\n          );\n        }\n      }\n    }\n  }\n\n  /// Starts auto-play if stopped, or stops it if currently running.\n  void toggleAutoPlay() {\n    if (isPlaying.value) {\n      stopAutoPlay();\n    } else {\n      startAutoPlay();\n    }\n  }\n\n  /// Begins animating through radar frames from [playStartTime] to\n  /// [playEndTime], looping continuously.\n  void startAutoPlay() {\n    if (isPlaying.value) return;\n\n    if (playStartTime.value == null) {\n      final radarList = GlobalProviders.data.radar;\n      if (radarList.isNotEmpty) {\n        playStartTime.value = radarList.last;\n      }\n    }\n\n    if (playStartTime.value != null && playEndTime.value != null) {\n      final radarList = GlobalProviders.data.radar;\n      final startIndex = radarList.indexOf(playStartTime.value);\n      final endIndex = radarList.indexOf(playEndTime.value);\n\n      if (startIndex != -1 && endIndex != -1 && startIndex <= endIndex) {\n        playEndTime.value = currentRadarTime.value;\n      }\n    } else if (playEndTime.value == null) {\n      playEndTime.value = currentRadarTime.value;\n    }\n\n    if (playStartTime.value == null || playEndTime.value == null) {\n      TalkerManager.instance.error(\n        'Cannot start auto-play: missing start or end time',\n      );\n      return;\n    }\n\n    _updateRadarTileUrl(playStartTime.value!);\n\n    isPlaying.value = true;\n    _playTimer = Timer.periodic(const Duration(milliseconds: 800), (timer) {\n      _playNext();\n    });\n\n    TalkerManager.instance.info(\n      'Started radar auto-play from: ${playStartTime.value} to: ${playEndTime.value}',\n    );\n  }\n\n  /// Stops the auto-play animation and clears the play range.\n  void stopAutoPlay() {\n    if (!isPlaying.value) return;\n\n    isPlaying.value = false;\n    _playTimer?.cancel();\n    _playTimer = null;\n    _isWaitingForRestart = false;\n\n    playStartTime.value = null;\n    playEndTime.value = null;\n\n    TalkerManager.instance.info('Stopped radar auto-play');\n  }\n\n  bool _isWaitingForRestart = false;\n\n  void _playNext() {\n    final radarList = GlobalProviders.data.radar;\n    if (radarList.isEmpty || currentRadarTime.value == null || _isWaitingForRestart) return;\n\n    final currentIndex = radarList.indexOf(currentRadarTime.value);\n    if (currentIndex == -1) return;\n\n    final startIndex = playStartTime.value != null ? radarList.indexOf(playStartTime.value) : -1;\n    final endIndex = playEndTime.value != null ? radarList.indexOf(playEndTime.value) : -1;\n\n    if (startIndex == -1 || endIndex == -1) {\n      if (playStartTime.value != null) {\n        _updateRadarTileUrl(playStartTime.value!);\n      }\n      return;\n    }\n\n    final nextIndex = currentIndex - 1;\n\n    if (nextIndex >= endIndex) {\n      final nextTime = radarList[nextIndex];\n      _updateRadarTileUrl(nextTime);\n    } else {\n      _isWaitingForRestart = true;\n      TalkerManager.instance.info(\n        'Reached end time, scheduling restart in 1 second',\n      );\n      Timer(const Duration(milliseconds: 1000), () {\n        if (isPlaying.value && playStartTime.value != null && _isWaitingForRestart) {\n          TalkerManager.instance.info(\n            'Restarting from start time: ${playStartTime.value}',\n          );\n          _updateRadarTileUrl(playStartTime.value!);\n          _isWaitingForRestart = false;\n        } else {\n          TalkerManager.instance.info(\n            'Restart cancelled - playing: ${isPlaying.value}, startTime: ${playStartTime.value}, waiting: $_isWaitingForRestart',\n          );\n        }\n      });\n    }\n  }\n\n  Future<void> _focus() async {\n    try {\n      final location = GlobalProviders.location.coordinates;\n\n      if (location != null && location.isValid) {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(location, 7.4),\n        );\n      } else {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n        );\n        TalkerManager.instance.info('Moved Camera to ${DpipMap.kTaiwanCenter}');\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager._focus', e, s);\n    }\n  }\n\n  Future<void> _fetchData() async {\n    final radarList = (await ExpTech().getRadarList()).reversed.toList();\n    if (!context.mounted) return;\n\n    GlobalProviders.data.setRadar(radarList);\n    currentRadarTime.value ??= radarList.first;\n    _lastFetchTime = DateTime.now();\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    try {\n      if (GlobalProviders.data.radar.isEmpty) await _fetchData();\n\n      final time = currentRadarTime.value;\n      if (time == null) throw Exception('Time is null');\n\n      await _setupAndShowLayer(time);\n      await _preloadAdjacentLayers(time);\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    stopAutoPlay();\n\n    try {\n      for (final time in _preloadedLayers) {\n        await _hideLayer(time);\n      }\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    try {\n      if (currentRadarTime.value != null) {\n        final layerId = MapLayerIds.radar(currentRadarTime.value);\n        await controller.setLayerVisibility(layerId, true);\n      }\n\n      await _focus();\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5)\n        await _fetchData();\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    stopAutoPlay();\n\n    try {\n      for (final time in _preloadedLayers.toList()) {\n        final layerId = MapLayerIds.radar(time);\n        final sourceId = MapSourceIds.radar(time);\n\n        await controller.removeLayer(layerId).catchError((_) {});\n        await controller.removeSource(sourceId).catchError((_) {});\n        TalkerManager.instance.info(\n          'Removed radar layer and source for \"$time\"',\n        );\n      }\n\n      _preloadedLayers.clear();\n    } catch (e, s) {\n      TalkerManager.instance.error('RadarMapLayerManager.remove', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  void dispose() {\n    stopAutoPlay();\n    currentRadarTime.dispose();\n    isLoading.dispose();\n    isPlaying.dispose();\n    playStartTime.dispose();\n    playEndTime.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (isMultiLayerMode) {\n      return const SizedBox.shrink();\n    }\n    return RadarMapLayerSheet(manager: this);\n  }\n}\n\nclass _LegendItem extends StatelessWidget {\n  final String label;\n  final Color color;\n  final Color borderColor;\n\n  const _LegendItem({\n    required this.label,\n    required this.color,\n    required this.borderColor,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: .min,\n      spacing: 4,\n      children: [\n        Container(\n          width: 14,\n          height: 14,\n          decoration: BoxDecoration(\n            color: color,\n            borderRadius: .circular(4),\n            border: Border.all(color: borderColor),\n          ),\n        ),\n        Text(\n          label,\n          style: context.texts.bodySmall?.copyWith(\n            color: context.colors.onSurfaceVariant,\n            height: 1,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\n/// The bottom sheet and legend overlay for the radar echo layer.\nclass RadarMapLayerSheet extends StatelessWidget {\n  /// The [RadarMapLayerManager] whose state drives this sheet.\n  final RadarMapLayerManager manager;\n\n  /// Creates a [RadarMapLayerSheet] for the given [manager].\n  const RadarMapLayerSheet({super.key, required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        MorphingSheet(\n          title: '雷達回波'.i18n,\n          borderRadius: .circular(16),\n          elevation: 4,\n          partialBuilder: (context, controller, sheetController) {\n            return Padding(\n              padding: const .only(top: 4, bottom: 8),\n              child: Selector<DpipDataModel, UnmodifiableListView<String>>(\n                selector: (context, model) => model.radar,\n                builder: (context, radar, child) {\n                  final times = radar.map((time) {\n                    final t = time.toSimpleDateTimeString().split(' ');\n                    return (date: t[0], time: t[1], value: time);\n                  });\n                  final grouped = times.groupListsBy((time) => time.date).entries.toList();\n\n                  return Column(\n                    mainAxisSize: .min,\n                    crossAxisAlignment: .start,\n                    children: [\n                      Padding(\n                        padding: const .only(left: 16, right: 4),\n                        child: SizedBox(\n                          height: 48,\n                          child: Row(\n                            mainAxisAlignment: .spaceBetween,\n                            children: [\n                              Row(\n                                spacing: 8,\n                                children: [\n                                  Icon(\n                                    Symbols.radar_rounded,\n                                    size: 24,\n                                    color: context.colors.onSurface,\n                                  ),\n                                  Text(\n                                    '雷達回波'.i18n,\n                                    style: context.texts.titleMedium?.copyWith(\n                                      color: context.colors.onSurface,\n                                    ),\n                                  ),\n                                  AnimatedBuilder(\n                                    animation: Listenable.merge([\n                                      manager.currentRadarTime,\n                                      manager.playStartTime,\n                                      manager.isPlaying,\n                                    ]),\n                                    builder: (context, child) {\n                                      final currentTime = manager.currentRadarTime.value;\n\n                                      if (currentTime == null) return const SizedBox.shrink();\n\n                                      try {\n                                        final timeFormatted = currentTime.toSimpleDateTimeString();\n                                        final timeData = timeFormatted.split(\n                                          ' ',\n                                        );\n                                        final date = timeData.length > 1 ? timeData[0] : '';\n                                        final time = timeData.length > 1\n                                            ? timeData[1]\n                                            : timeData[0];\n\n                                        return Container(\n                                          padding: const .symmetric(\n                                            horizontal: 8,\n                                            vertical: 5,\n                                          ),\n                                          decoration: BoxDecoration(\n                                            color: context.colors.surfaceContainer.withValues(\n                                              alpha: 0.6,\n                                            ),\n                                            borderRadius: .circular(\n                                              8,\n                                            ),\n                                            border: Border.all(\n                                              color: context.colors.outline,\n                                            ),\n                                          ),\n                                          child: Row(\n                                            mainAxisSize: .min,\n                                            spacing: 4,\n                                            children: [\n                                              Icon(\n                                                Icons.schedule_rounded,\n                                                size: 12,\n                                                color: context.colors.onSurfaceVariant,\n                                              ),\n                                              if (date.isNotEmpty) ...[\n                                                Text(\n                                                  date,\n                                                  style: context.texts.labelSmall?.copyWith(\n                                                    color: context.colors.onSurfaceVariant,\n                                                    height: 1,\n                                                  ),\n                                                ),\n                                                Container(\n                                                  width: 0.5,\n                                                  height: 14,\n                                                  margin: const EdgeInsets.symmetric(\n                                                    horizontal: 2,\n                                                  ),\n                                                  color: context.colors.outline,\n                                                ),\n                                              ],\n                                              Text(\n                                                time,\n                                                style: context.texts.bodySmall?.copyWith(\n                                                  color: context.colors.onSurface,\n                                                  fontWeight: FontWeight.bold,\n                                                  height: 1,\n                                                ),\n                                              ),\n                                            ],\n                                          ),\n                                        );\n                                      } catch (e) {\n                                        return const SizedBox.shrink();\n                                      }\n                                    },\n                                  ),\n                                ],\n                              ),\n                              AnimatedBuilder(\n                                animation: Listenable.merge([\n                                  manager.isPlaying,\n                                  manager.playStartTime,\n                                  manager.currentRadarTime,\n                                ]),\n                                builder: (context, child) {\n                                  final isPlaying = manager.isPlaying.value;\n                                  final startTime = manager.playStartTime.value;\n                                  final canPlay = manager.canPlay;\n\n                                  final shouldHide = startTime == null && !isPlaying;\n\n                                  if (shouldHide) {\n                                    return const SizedBox.shrink();\n                                  }\n\n                                  return IconButton(\n                                    onPressed: canPlay || isPlaying ? manager.toggleAutoPlay : null,\n                                    icon: Icon(\n                                      isPlaying\n                                          ? Symbols.pause_rounded\n                                          : Symbols.play_arrow_rounded,\n                                      size: 24,\n                                      color: context.colors.primary,\n                                    ),\n                                  );\n                                },\n                              ),\n                            ],\n                          ),\n                        ),\n                      ),\n                      AnimatedBuilder(\n                        animation: Listenable.merge([\n                          manager.playStartTime,\n                          manager.isPlaying,\n                        ]),\n                        builder: (context, child) {\n                          final startTime = manager.playStartTime.value;\n                          final isPlaying = manager.isPlaying.value;\n\n                          if (isPlaying) {\n                            return const SizedBox.shrink();\n                          }\n\n                          if (startTime == null && !isPlaying) {\n                            return Padding(\n                              padding: const .fromLTRB(16, 4, 16, 8),\n                              child: Text(\n                                '長按設定播放起點'.i18n,\n                                style: context.texts.bodySmall?.copyWith(\n                                  color: context.colors.onSurfaceVariant,\n                                ),\n                              ),\n                            );\n                          }\n\n                          return Padding(\n                            padding: const .fromLTRB(16, 4, 16, 8),\n                            child: Column(\n                              children: [\n                                Row(\n                                  spacing: 8,\n                                  children: [\n                                    _LegendItem(\n                                      label: '目前時間'.i18n,\n                                      color: context.colors.primaryContainer,\n                                      borderColor: context.colors.primary,\n                                    ),\n                                    _LegendItem(\n                                      label: '播放起點'.i18n,\n                                      color: context.colors.tertiaryContainer,\n                                      borderColor: context.colors.tertiary,\n                                    ),\n                                  ],\n                                ),\n                              ],\n                            ),\n                          );\n                        },\n                      ),\n                      ValueListenableBuilder<bool>(\n                        valueListenable: manager.isPlaying,\n                        builder: (context, isPlaying, child) {\n                          if (isPlaying) {\n                            return Container(\n                              height: 32,\n                              padding: const .symmetric(\n                                horizontal: 16,\n                                vertical: 4,\n                              ),\n                              child: AnimatedBuilder(\n                                animation: Listenable.merge([\n                                  manager.currentRadarTime,\n                                  manager.playStartTime,\n                                  manager.playEndTime,\n                                ]),\n                                builder: (context, child) {\n                                  return _RadarProgressBar(manager: manager);\n                                },\n                              ),\n                            );\n                          }\n\n                          return SizedBox(\n                            height: kMinInteractiveDimension,\n                            child: ValueListenableBuilder<String?>(\n                              valueListenable: manager.currentRadarTime,\n                              builder: (context, currentTime, child) {\n                                return ValueListenableBuilder<String?>(\n                                  valueListenable: manager.playStartTime,\n                                  builder: (context, startTime, child) {\n                                    return _AutoScrollingTimeList(\n                                      grouped: grouped,\n                                      currentTime: currentTime,\n                                      startTime: startTime,\n                                      manager: manager,\n                                      shouldFocusOnShow: true,\n                                    );\n                                  },\n                                );\n                              },\n                            ),\n                          );\n                        },\n                      ),\n                    ],\n                  );\n                },\n              ),\n            );\n          },\n        ),\n        Positioned(\n          top: 24 + 48 + 16,\n          left: 24,\n          child: SafeArea(\n            child: BlurredContainer(\n              elevation: 4,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n              child: ColorLegend(\n                reverse: true,\n                unit: 'dBZ',\n                items: [\n                  ColorLegendItem(color: const Color(0xff00ffff), value: 0),\n                  ColorLegendItem(color: const Color(0xff00a3ff), value: 5),\n                  ColorLegendItem(color: const Color(0xff005bff), value: 10),\n                  ColorLegendItem(\n                    color: const Color(0xff0000ff),\n                    value: 15,\n                    blendTail: false,\n                  ),\n                  ColorLegendItem(\n                    color: const Color(0xff00ff00),\n                    value: 16,\n                    hidden: true,\n                  ),\n                  ColorLegendItem(color: const Color(0xff00d300), value: 20),\n                  ColorLegendItem(color: const Color(0xff00a000), value: 25),\n                  ColorLegendItem(color: const Color(0xffccea00), value: 30),\n                  ColorLegendItem(color: const Color(0xffffd300), value: 35),\n                  ColorLegendItem(color: const Color(0xffff8800), value: 40),\n                  ColorLegendItem(color: const Color(0xffff1800), value: 45),\n                  ColorLegendItem(color: const Color(0xffd30000), value: 50),\n                  ColorLegendItem(color: const Color(0xffa00000), value: 55),\n                  ColorLegendItem(color: const Color(0xffea00cc), value: 60),\n                  ColorLegendItem(color: const Color(0xff9600ff), value: 65),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _AutoScrollingTimeList extends StatefulWidget {\n  final List<MapEntry<String, List<({String date, String time, String value})>>> grouped;\n  final String? currentTime;\n  final String? startTime;\n  final RadarMapLayerManager manager;\n  final bool shouldFocusOnShow;\n\n  const _AutoScrollingTimeList({\n    required this.grouped,\n    required this.currentTime,\n    required this.startTime,\n    required this.manager,\n    this.shouldFocusOnShow = false,\n  });\n\n  @override\n  State<_AutoScrollingTimeList> createState() => _AutoScrollingTimeListState();\n}\n\nclass _AutoScrollingTimeListState extends State<_AutoScrollingTimeList> {\n  final ScrollController _scrollController = ScrollController();\n  final Map<String, GlobalKey> _chipKeys = {};\n  String? _lastCurrentTime;\n\n  @override\n  void initState() {\n    super.initState();\n    for (final group in widget.grouped) {\n      for (final time in group.value) {\n        _chipKeys[time.value] = GlobalKey();\n      }\n    }\n\n    if (widget.shouldFocusOnShow && widget.currentTime != null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        Future.delayed(const Duration(milliseconds: 100), () {\n          if (mounted) {\n            _scrollToCurrentTime();\n          }\n        });\n      });\n    }\n  }\n\n  @override\n  void didUpdateWidget(_AutoScrollingTimeList oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (widget.currentTime != _lastCurrentTime && widget.currentTime != null) {\n      _lastCurrentTime = widget.currentTime;\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        _scrollToCurrentTime();\n      });\n    }\n  }\n\n  void _scrollToCurrentTime() {\n    if (widget.currentTime == null || !_scrollController.hasClients) return;\n\n    final key = _chipKeys[widget.currentTime];\n    if (key?.currentContext == null) return;\n\n    final RenderBox? renderBox = key!.currentContext!.findRenderObject() as RenderBox?;\n    if (renderBox == null) return;\n\n    final RenderBox? scrollViewBox =\n        _scrollController.position.context.storageContext.findRenderObject() as RenderBox?;\n    if (scrollViewBox == null) return;\n\n    if (!renderBox.attached) return;\n\n    final position = renderBox.localToGlobal(Offset.zero);\n    final localPosition = scrollViewBox.globalToLocal(position);\n\n    final targetOffset =\n        _scrollController.offset +\n        localPosition.dx -\n        (scrollViewBox.size.width / 2) +\n        (renderBox.size.width / 2);\n\n    final clampedOffset = targetOffset.clamp(\n      _scrollController.position.minScrollExtent,\n      _scrollController.position.maxScrollExtent,\n    );\n\n    if ((clampedOffset - _scrollController.offset).abs() > 20) {\n      _scrollController.animateTo(\n        clampedOffset,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      controller: _scrollController,\n      padding: const .symmetric(horizontal: 16),\n      scrollDirection: .horizontal,\n      physics: const AlwaysScrollableScrollPhysics(),\n      itemCount: widget.grouped.length,\n      itemBuilder: (context, index) {\n        final MapEntry(key: date, value: group) = widget.grouped[index];\n\n        final children = <Widget>[Text(date)];\n\n        for (final time in group) {\n          final isSelected = time.value == widget.currentTime;\n          final isStartTime = time.value == widget.startTime;\n\n          children.add(\n            ValueListenableBuilder<bool>(\n              valueListenable: widget.manager.isLoading,\n              builder: (context, isLoading, child) {\n                Color chipColor;\n                Color borderColor;\n                Color textColor;\n\n                if (isSelected) {\n                  chipColor = context.colors.primaryContainer;\n                  borderColor = context.colors.primary;\n                  textColor = context.colors.onPrimaryContainer;\n                } else if (isStartTime) {\n                  chipColor = context.colors.tertiaryContainer;\n                  borderColor = context.colors.tertiary;\n                  textColor = context.colors.onTertiaryContainer;\n                } else {\n                  chipColor = context.colors.surface.withValues(alpha: 0.6);\n                  borderColor = context.colors.outlineVariant;\n                  textColor = context.colors.onSurfaceVariant;\n                }\n\n                return GestureDetector(\n                  onLongPress: isLoading\n                      ? null\n                      : () {\n                          widget.manager.setPlayStartTime(time.value);\n                        },\n                  child: FilterChip(\n                    key: _chipKeys[time.value],\n                    selected: isSelected,\n                    showCheckmark: !isLoading,\n                    label: Text(time.time, style: TextStyle(color: textColor)),\n                    backgroundColor: chipColor,\n                    side: BorderSide(color: borderColor),\n                    avatar: isSelected && isLoading ? const LoadingIcon() : null,\n                    onSelected: isLoading\n                        ? null\n                        : (selected) {\n                            if (!selected) return;\n                            widget.manager.updateRadarTime(time.value);\n                          },\n                  ),\n                );\n              },\n            ),\n          );\n        }\n\n        children.add(\n          const Padding(\n            padding: .only(right: 8),\n            child: VerticalDivider(width: 16, indent: 8, endIndent: 8),\n          ),\n        );\n\n        return Row(\n          mainAxisSize: .min,\n          spacing: 8,\n          children: children,\n        );\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n}\n\nclass _RadarProgressBar extends StatelessWidget {\n  final RadarMapLayerManager manager;\n\n  const _RadarProgressBar({required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    final currentTime = manager.currentRadarTime.value;\n    final startTime = manager.playStartTime.value;\n    final endTime = manager.playEndTime.value;\n\n    if (currentTime == null || startTime == null || endTime == null) {\n      return const SizedBox.shrink();\n    }\n\n    final radarList = GlobalProviders.data.radar;\n    final currentIndex = radarList.indexOf(currentTime);\n    final startIndex = radarList.indexOf(startTime);\n    final endIndex = radarList.indexOf(endTime);\n\n    if (currentIndex == -1 || startIndex == -1 || endIndex == -1) {\n      return const SizedBox.shrink();\n    }\n\n    double progress = 0.0;\n    if (startIndex != endIndex) {\n      progress = (startIndex - currentIndex) / (startIndex - endIndex);\n      progress = progress.clamp(0.0, 1.0);\n    }\n\n    return Row(\n      spacing: 8,\n      children: [\n        Icon(\n          Icons.play_circle_rounded,\n          size: 16,\n          color: context.colors.primary,\n        ),\n        Text(\n          '播放進度'.i18n,\n          style: context.texts.labelSmall?.copyWith(\n            color: context.colors.onSurface,\n            height: 1,\n          ),\n        ),\n        Expanded(\n          child: LinearProgressIndicator(value: progress, year2023: false),\n        ),\n        Text(\n          '${(progress * 100).round()}%',\n          style: context.texts.bodySmall?.copyWith(\n            color: context.colors.onSurfaceVariant,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/report.dart",
    "content": "/// Map layer manager and associated UI for earthquake report data.\nlibrary;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/api/model/report/earthquake_report.dart';\nimport 'package:dpip/api/model/report/partial_earthquake_report.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/page.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/depth_color.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:dpip/utils/extensions/iterable.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/magnitude_color.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/report/enlargeable_image.dart';\nimport 'package:dpip/widgets/report/intensity_box.dart';\nimport 'package:dpip/widgets/responsive/responsive_container.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet_controller.dart';\nimport 'package:dpip/widgets/typography.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:intl/intl.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// Manages the earthquake report overlay layer on the DPIP map.\nclass ReportMapLayerManager extends MapLayerManager {\n  /// The report ID to select immediately when the layer is first shown.\n  String? initialReportId;\n\n  /// Creates a [ReportMapLayerManager], optionally with an [initialReportId].\n  ReportMapLayerManager(\n    super.context,\n    super.controller, {\n    this.initialReportId,\n  });\n\n  /// The currently selected partial report, or `null` when viewing the list.\n  final currentReport = ValueNotifier<PartialEarthquakeReport?>(null);\n\n  /// Whether a report load operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  /// Incremented each time the report list is updated; drives UI rebuilds.\n  final dataNotifier = ValueNotifier<int>(0);\n\n  /// Whether the sheet should expand when the user returns from a detail view.\n  final shouldExpandOnReturn = ValueNotifier<bool>(false);\n\n  /// The scroll offset saved before navigating to a report detail view.\n  double savedScrollOffset = 0.0;\n  String? _lastPartialContentKey;\n  bool _shouldResetScroll = false;\n\n  DateTime? _lastFetchTime;\n  int _currentPage = 1;\n\n  /// Whether more pages of report data are available to load.\n  final hasMore = ValueNotifier<bool>(true);\n\n  /// Whether a pagination load is currently in progress.\n  final isLoadingMore = ValueNotifier<bool>(false);\n  static const int _pageSize = 50;\n\n  /// Selects the report with [reportId], or clears the selection if `null`.\n  ///\n  /// Optionally animates the camera to the report bounds when [focus] is\n  /// `true`.\n  Future<void> setReport(String? reportId, {bool focus = true}) async {\n    if (isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      await _removeReport(currentReport.value, focus: focus);\n\n      PartialEarthquakeReport? report;\n      if (reportId != null) {\n        report = GlobalProviders.data.partialReport.firstWhereOrNull(\n          (r) => r.id == reportId,\n        );\n      }\n\n      currentReport.value = report;\n\n      if (report != null) {\n        _shouldResetScroll = true;\n        await _addReport(currentReport.value, focus: focus);\n      }\n\n      TalkerManager.instance.info('Updated report to \"$reportId\"');\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager.setReport', e, s);\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future<void> _focus([EarthquakeReport? report]) async {\n    if (report != null) {\n      await controller.animateCamera(\n        CameraUpdate.newLatLngBounds(\n          report.bounds,\n          left: 48,\n          right: 48,\n          top: 96,\n          bottom: 192,\n        ),\n      );\n      return;\n    }\n\n    final data = GlobalProviders.data.partialReport;\n    var bounds = <double>[];\n\n    for (final report in data) {\n      if (bounds.isEmpty) {\n        bounds = [\n          report.latitude,\n          report.longitude,\n          report.latitude,\n          report.longitude,\n        ];\n      } else {\n        bounds.expandBounds(report.latlng);\n      }\n    }\n\n    await controller.animateCamera(\n      CameraUpdate.newLatLngBounds(\n        bounds.asLatLngBounds,\n        left: 48,\n        right: 48,\n        top: 96,\n        bottom: 192,\n      ),\n    );\n  }\n\n  Future<void> _fetchData({bool reset = false}) async {\n    if (reset) {\n      _currentPage = 1;\n      hasMore.value = true;\n      isLoadingMore.value = false;\n    }\n\n    if (isLoadingMore.value || !hasMore.value) return;\n\n    isLoadingMore.value = true;\n\n    try {\n      final reportList = await ExpTech().getReportList(\n        limit: _pageSize,\n        page: _currentPage,\n      );\n      if (!context.mounted) return;\n\n      if (reportList.isEmpty) {\n        hasMore.value = false;\n      } else {\n        if (reset) {\n          GlobalProviders.data.setPartialReport(reportList);\n          _lastPartialContentKey = null;\n        } else {\n          GlobalProviders.data.appendPartialReport(reportList);\n        }\n\n        if (reportList.length < _pageSize) {\n          hasMore.value = false;\n        } else {\n          _currentPage++;\n        }\n\n        dataNotifier.value++;\n      }\n\n      _lastFetchTime = DateTime.now();\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager._fetchData', e, s);\n    } finally {\n      isLoadingMore.value = false;\n    }\n  }\n\n  /// Loads the next page of reports if not already loading and more are\n  /// available.\n  Future<void> loadMore() async {\n    if (!isLoadingMore.value && hasMore.value) {\n      await _fetchData();\n    }\n  }\n\n  String? _getPartialContentKey() {\n    final reports = GlobalProviders.data.partialReport;\n    if (reports.isEmpty) return null;\n    final firstReport = reports.first;\n    return '${firstReport.id}-${firstReport.time.millisecondsSinceEpoch}';\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    try {\n      if (GlobalProviders.data.partialReport.isEmpty) {\n        await _fetchData(reset: true);\n      }\n\n      final sourceId = MapSourceIds.report();\n      final layerId = MapLayerIds.report();\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (isSourceExists && isLayerExists) return;\n\n      if (!isSourceExists) {\n        final data = GeoJsonBuilder()\n            .setFeatures(\n              GlobalProviders.data.partialReport.reversed.map(\n                (report) => report.toGeoJsonFeature(),\n              ),\n            )\n            .build();\n\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        final properties = SymbolLayerProperties(\n          iconImage: [Expressions.get, 'icon'],\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.get, 'magnitude'],\n            1,\n            0.1,\n            10,\n            0.6,\n          ],\n          iconOpacity: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.get, 'time'],\n            DateTime.now().millisecondsSinceEpoch - const Duration(days: 14).inMilliseconds,\n            0.2,\n            GlobalProviders.data.partialReport.first.time.millisecondsSinceEpoch,\n            1.0,\n          ],\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          symbolZOrder: 'source',\n          visibility: visible && initialReportId == null ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    if (currentReport.value != null) {\n      await setReport(null, focus: false);\n    }\n\n    final layerId = MapLayerIds.report();\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final layerId = MapLayerIds.report();\n\n    try {\n      if (initialReportId != null) {\n        await setReport(initialReportId);\n        initialReportId = null;\n      } else {\n        await controller.setLayerVisibility(layerId, true);\n\n        await _focus();\n      }\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5) {\n        await _fetchData(reset: true);\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final layerId = MapLayerIds.report();\n      final sourceId = MapSourceIds.report();\n\n      await controller.removeLayer(layerId);\n      await controller.removeSource(sourceId);\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager.dispose', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  bool get shouldPop => currentReport.value == null;\n\n  @override\n  void onPopInvoked() {\n    if (currentReport.value == null) return;\n\n    shouldExpandOnReturn.value = true;\n    setReport(null);\n  }\n\n  Future<void> _addReport(\n    PartialEarthquakeReport? partial, {\n    bool focus = true,\n  }) async {\n    if (partial == null) return;\n\n    var report = GlobalProviders.data.report[partial.id];\n\n    try {\n      if (report == null) {\n        report = await ExpTech().getReport(partial.id);\n        if (!context.mounted) return;\n\n        GlobalProviders.data.setReport(partial.id, report);\n      }\n\n      final layerId = MapLayerIds.report(\n        report.time.millisecondsSinceEpoch.toString(),\n      );\n      final sourceId = MapSourceIds.report(\n        report.time.millisecondsSinceEpoch.toString(),\n      );\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (isSourceExists && isLayerExists) return;\n\n      if (!isSourceExists) {\n        final data = report.toGeoJson().build();\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        const properties = SymbolLayerProperties(\n          iconImage: [Expressions.get, 'icon'],\n          iconSize: kSymbolIconSize,\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          symbolZOrder: 'source',\n        );\n\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n      }\n\n      if (focus) await _focus(report);\n\n      await controller.setLayerVisibility(MapLayerIds.report(), false);\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager._addReport', e, s);\n    }\n  }\n\n  Future<void> _removeReport(\n    PartialEarthquakeReport? report, {\n    bool focus = true,\n  }) async {\n    if (report == null) return;\n\n    try {\n      final layerId = MapLayerIds.report(\n        report.time.millisecondsSinceEpoch.toString(),\n      );\n      final sourceId = MapSourceIds.report(\n        report.time.millisecondsSinceEpoch.toString(),\n      );\n\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n\n      if (isLayerExists) {\n        await controller.removeLayer(layerId);\n      }\n\n      if (isSourceExists) {\n        await controller.removeSource(sourceId);\n      }\n\n      if (focus) await _focus();\n\n      await controller.setLayerVisibility(MapLayerIds.report(), true);\n    } catch (e, s) {\n      TalkerManager.instance.error('ReportMapLayerManager._removeReport', e, s);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) => ReportMapLayerSheet(manager: this);\n}\n\n/// The bottom sheet that lists earthquake reports and shows report details.\nclass ReportMapLayerSheet extends StatefulWidget {\n  /// The [ReportMapLayerManager] whose state drives this sheet.\n  final ReportMapLayerManager manager;\n\n  /// Creates a [ReportMapLayerSheet] for the given [manager].\n  const ReportMapLayerSheet({super.key, required this.manager});\n\n  @override\n  State<ReportMapLayerSheet> createState() => _ReportMapLayerSheetState();\n}\n\n/// A widget that renders [builder] and hides itself when [builder] reports an\n/// error via the provided [VoidCallback].\nclass SafeImageSection extends StatefulWidget {\n  /// Builds the child widget; call the supplied callback to signal an error.\n  final Widget Function(VoidCallback onError) builder;\n\n  /// Creates a [SafeImageSection] with the given [builder].\n  const SafeImageSection({\n    super.key,\n    required this.builder,\n  });\n\n  @override\n  State<SafeImageSection> createState() => _SafeImageSectionState();\n}\n\nclass _SafeImageSectionState extends State<SafeImageSection> {\n  bool _hasError = false;\n  int _retryKey = 0;\n\n  void _onImageError() {\n    if (_hasError) return;\n    _hasError = true;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!mounted) return;\n      setState(() {});\n    });\n  }\n\n  void _retry() {\n    setState(() {\n      _hasError = false;\n      _retryKey++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_hasError) {\n      return _GeneratingView(onRetry: _retry);\n    }\n\n    return KeyedSubtree(\n      key: ValueKey(_retryKey),\n      child: widget.builder(_onImageError),\n    );\n  }\n}\n\nclass _GeneratingView extends StatelessWidget {\n  final VoidCallback onRetry;\n\n  const _GeneratingView({\n    required this.onRetry,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return AspectRatio(\n      aspectRatio: 2334 / 2977,\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Text('CWA 正在製圖中'.i18n),\n          const SizedBox(height: 12),\n          TextButton.icon(\n            onPressed: onRetry,\n            icon: const Icon(Icons.refresh),\n            label: Text('重新載入'.i18n),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _ReportMapLayerSheetState extends State<ReportMapLayerSheet> {\n  final morphingSheetController = MorphingSheetController();\n  ScrollController? _listScrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.manager.shouldExpandOnReturn.addListener(_onShouldExpandChanged);\n  }\n\n  @override\n  void dispose() {\n    widget.manager.shouldExpandOnReturn.removeListener(_onShouldExpandChanged);\n    _listScrollController?.removeListener(_onScroll);\n    super.dispose();\n  }\n\n  void _onScroll() {\n    if (_listScrollController == null || !_listScrollController!.hasClients) {\n      return;\n    }\n\n    final maxScroll = _listScrollController!.position.maxScrollExtent;\n    final currentScroll = _listScrollController!.offset;\n    final delta = 200.0;\n\n    if (maxScroll - currentScroll <= delta) {\n      widget.manager.loadMore();\n    }\n  }\n\n  void _onShouldExpandChanged() {\n    if (!widget.manager.shouldExpandOnReturn.value) return;\n    widget.manager.shouldExpandOnReturn.value = false;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!mounted) return;\n      morphingSheetController.expand().then((_) {\n        if (!mounted) return;\n        final offset = widget.manager.savedScrollOffset;\n        if (_listScrollController != null && _listScrollController!.hasClients && offset > 0) {\n          _listScrollController!.jumpTo(offset);\n        }\n      });\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: widget.manager.dataNotifier,\n      builder: (context, value, child) {\n        final partialKey = widget.manager._getPartialContentKey();\n        final shouldUpdateKey = partialKey != widget.manager._lastPartialContentKey;\n        if (shouldUpdateKey) {\n          widget.manager._lastPartialContentKey = partialKey;\n        }\n\n        return ResponsiveContainer(\n          mode: ResponsiveMode.panel,\n          child: MorphingSheet(\n            key: partialKey != null ? ValueKey(partialKey) : null,\n            controller: morphingSheetController,\n            title: '地震報告'.i18n,\n            borderRadius: .circular(16),\n            elevation: 4,\n            partialBuilder: (context, controller, sheetController) {\n              if (GlobalProviders.data.partialReport.isEmpty) {\n                return const SizedBox.shrink();\n              }\n\n              return ValueListenableBuilder(\n                valueListenable: widget.manager.currentReport,\n                builder: (context, currentReport, child) {\n                  // Show the first report from partial report list\n                  if (currentReport == null) {\n                    final report = GlobalProviders.data.partialReport.first;\n\n                    final locationString = report.extractLocation();\n                    final location =\n                        Location.tryParse(locationString)?.dynamicName ?? locationString;\n\n                    return Padding(\n                      padding: const .symmetric(vertical: 8),\n                      child: Selector<DpipDataModel, List<String>>(\n                        selector: (context, model) => model.radar,\n                        builder: (context, radar, child) {\n                          return Column(\n                            mainAxisSize: MainAxisSize.min,\n                            spacing: 4,\n                            children: [\n                              Padding(\n                                padding: const .symmetric(\n                                  horizontal: 16,\n                                  vertical: 8,\n                                ),\n                                child: Row(\n                                  spacing: 8,\n                                  children: [\n                                    Icon(\n                                      Symbols.docs_rounded,\n                                      size: 24,\n                                      color: context.colors.onSurface,\n                                    ),\n                                    Expanded(\n                                      child: Text(\n                                        '近期的地震報告'.i18n,\n                                        style: context.texts.titleMedium?.copyWith(\n                                          color: context.colors.onSurface,\n                                        ),\n                                      ),\n                                    ),\n                                    Text(\n                                      '更多'.i18n,\n                                      style: context.texts.labelSmall?.copyWith(\n                                        color: context.colors.outline,\n                                      ),\n                                    ),\n                                    Icon(\n                                      Symbols.swipe_up_rounded,\n                                      size: 16,\n                                      color: context.colors.outline,\n                                    ),\n                                  ],\n                                ),\n                              ),\n                              Padding(\n                                padding: const .symmetric(\n                                  horizontal: 16,\n                                ),\n                                child: Row(\n                                  spacing: 8,\n                                  children: [\n                                    IntensityBox(\n                                      intensity: report.intensity,\n                                      size: 48,\n                                      borderRadius: 12,\n                                      border: !report.hasNumber,\n                                    ),\n                                    Expanded(\n                                      child: Column(\n                                        mainAxisSize: MainAxisSize.min,\n                                        crossAxisAlignment: CrossAxisAlignment.start,\n                                        children: [\n                                          Text(\n                                            report.hasNumber\n                                                ? '編號 {number} 顯著有感地震'.i18n.args({\n                                                    'number': report.number,\n                                                  })\n                                                : location,\n                                            style: context.texts.titleMedium,\n                                          ),\n                                          Text(\n                                            report.time.toLocaleDateTimeString(\n                                              context,\n                                            ),\n                                            style: context.texts.bodyMedium?.copyWith(\n                                              color: context.colors.onSurfaceVariant,\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                    ),\n                                    Text(\n                                      'M ${report.magnitude.toStringAsFixed(1)}',\n                                      style: context.texts.titleMedium,\n                                    ),\n                                  ],\n                                ),\n                              ),\n                            ],\n                          );\n                        },\n                      ),\n                    );\n                  }\n\n                  // Show the current report with details\n\n                  final locationString = currentReport.extractLocation();\n                  final location = Location.tryParse(locationString)?.dynamicName ?? locationString;\n\n                  return Padding(\n                    padding: const EdgeInsets.all(12),\n                    child: Column(\n                      mainAxisSize: MainAxisSize.min,\n                      spacing: 4,\n                      children: [\n                        Row(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          spacing: 12,\n                          children: [\n                            Expanded(\n                              child: Column(\n                                mainAxisSize: MainAxisSize.min,\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                spacing: 2,\n                                children: [\n                                  Text(\n                                    currentReport.hasNumber\n                                        ? '編號 {number} 顯著有感地震'.i18n.args({\n                                            'number': currentReport.number,\n                                          })\n                                        : '小區域有感地震'.i18n,\n                                    style: context.texts.labelMedium?.copyWith(\n                                      color: context.colors.outline,\n                                    ),\n                                  ),\n                                  Text(\n                                    location,\n                                    style: context.texts.titleLarge?.copyWith(\n                                      fontWeight: FontWeight.w500,\n                                    ),\n                                  ),\n                                  Text(\n                                    currentReport.time.toLocaleDateTimeString(\n                                      context,\n                                    ),\n                                    style: context.texts.bodyMedium?.copyWith(\n                                      color: context.colors.onSurfaceVariant,\n                                    ),\n                                  ),\n                                ],\n                              ),\n                            ),\n                            IntensityBox(\n                              intensity: currentReport.intensity,\n                              size: 56,\n                              borderRadius: 12,\n                              border: !currentReport.hasNumber,\n                            ),\n                          ],\n                        ),\n                        Row(\n                          spacing: 16,\n                          children: [\n                            Expanded(\n                              child: Row(\n                                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                                children: [\n                                  Text(\n                                    '地震規模'.i18n,\n                                    style: context.texts.bodyMedium?.copyWith(\n                                      color: context.colors.onSurfaceVariant,\n                                    ),\n                                  ),\n                                  Text(\n                                    'M ${currentReport.magnitude.toStringAsFixed(1)}',\n                                    style: context.texts.bodyLarge?.copyWith(\n                                      fontWeight: FontWeight.w500,\n                                    ),\n                                  ),\n                                ],\n                              ),\n                            ),\n                            Expanded(\n                              child: Row(\n                                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                                children: [\n                                  Text(\n                                    '震源深度'.i18n,\n                                    style: context.texts.bodyMedium?.copyWith(\n                                      color: context.colors.onSurfaceVariant,\n                                    ),\n                                  ),\n                                  Text(\n                                    '${currentReport.depth}km',\n                                    style: context.texts.bodyLarge?.copyWith(\n                                      fontWeight: FontWeight.w500,\n                                    ),\n                                  ),\n                                ],\n                              ),\n                            ),\n                          ],\n                        ),\n                      ],\n                    ),\n                  );\n                },\n              );\n            },\n            fullBuilder: (context, controller, sheetController) {\n              return ValueListenableBuilder(\n                valueListenable: widget.manager.currentReport,\n                builder: (context, currentReport, child) {\n                  if (currentReport == null) {\n                    if (_listScrollController != controller) {\n                      _listScrollController?.removeListener(_onScroll);\n                      _listScrollController = controller;\n                      controller.addListener(_onScroll);\n                    }\n\n                    final grouped = GlobalProviders.data.partialReport\n                        .groupListsBy(\n                          (report) => report.time.toLocaleFullDateString(context),\n                        )\n                        .entries\n                        .toList();\n\n                    return CustomScrollView(\n                      controller: controller,\n                      slivers: [\n                        SliverAppBar(\n                          title: Text('地震報告'.i18n),\n                          leading: BackButton(\n                            onPressed: () {\n                              sheetController.collapse();\n                              controller.animateTo(\n                                0,\n                                duration: Durations.short4,\n                                curve: Easing.emphasizedDecelerate,\n                              );\n                            },\n                          ),\n                          floating: true,\n                          snap: true,\n                          pinned: true,\n                        ),\n                        SliverPadding(\n                          padding: .only(\n                            bottom: context.padding.bottom,\n                          ),\n                          sliver: SliverList.builder(\n                            itemCount: grouped.length + 1,\n                            itemBuilder: (context, index) {\n                              if (index == grouped.length) {\n                                return ValueListenableBuilder(\n                                  valueListenable: widget.manager.isLoadingMore,\n                                  builder: (context, isLoadingMoreValue, child) {\n                                    return ValueListenableBuilder(\n                                      valueListenable: widget.manager.hasMore,\n                                      builder: (context, hasMoreValue, child) {\n                                        if (!hasMoreValue &&\n                                            GlobalProviders.data.partialReport.isNotEmpty) {\n                                          return Padding(\n                                            padding: const EdgeInsets.all(16),\n                                            child: Center(\n                                              child: Text(\n                                                '沒有更多資料'.i18n,\n                                                style: context.texts.bodyMedium?.copyWith(\n                                                  color: context.colors.onSurfaceVariant,\n                                                ),\n                                              ),\n                                            ),\n                                          );\n                                        }\n\n                                        if (isLoadingMoreValue) {\n                                          return const Padding(\n                                            padding: EdgeInsets.all(16),\n                                            child: Center(\n                                              child: CircularProgressIndicator(),\n                                            ),\n                                          );\n                                        }\n\n                                        return const SizedBox.shrink();\n                                      },\n                                    );\n                                  },\n                                );\n                              }\n\n                              final MapEntry(key: date, value: reports) = grouped[index];\n\n                              final length = reports.length;\n\n                              return SegmentedList(\n                                label: Text(date),\n                                children: reports.mapIndexed((index, report) {\n                                  final locationString = report.extractLocation();\n                                  final location =\n                                      Location.tryParse(\n                                        locationString,\n                                      )?.dynamicName ??\n                                      locationString;\n\n                                  return SegmentedListTile(\n                                    isFirst: index == 0,\n                                    isLast: index == length - 1,\n                                    leading: IntensityBox(\n                                      intensity: report.intensity,\n                                      size: 36,\n                                      borderRadius: 8,\n                                      border: !report.hasNumber,\n                                    ),\n                                    title: Text(location),\n                                    subtitle: Text(\n                                      '${report.hasNumber ? '${'編號 {number} 顯著有感地震'.i18n.args({'number': report.number})}\\n' : ''}${report.time.toLocaleTimeString(context)}・${report.depth}km',\n                                    ),\n                                    trailing: Text(\n                                      'M ${report.magnitude.toStringAsFixed(1)}',\n                                      style: context.texts.labelLarge,\n                                    ),\n                                    onTap: () {\n                                      if (controller.hasClients) {\n                                        widget.manager.savedScrollOffset = controller.offset;\n                                      }\n                                      widget.manager.setReport(report.id);\n                                      sheetController.collapse();\n                                    },\n                                  );\n                                }).toList(),\n                              );\n                            },\n                          ),\n                        ),\n                      ],\n                    );\n                  }\n\n                  final report = GlobalProviders.data.report[currentReport.id];\n                  late List<Widget> content;\n\n                  if (report == null) {\n                    content = [\n                      const Center(child: CircularProgressIndicator()),\n                    ];\n                  } else {\n                    final locationString = report.getLocation();\n                    final location =\n                        Location.tryParse(locationString)?.dynamicName ?? locationString;\n\n                    content = [\n                      Padding(\n                        padding: const .symmetric(\n                          horizontal: 24,\n                          vertical: 8,\n                        ),\n                        child: Row(\n                          children: [\n                            IntensityBox(intensity: report.getMaxIntensity()),\n                            const SizedBox(width: 16),\n                            Expanded(\n                              child: Column(\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: [\n                                  Text(\n                                    report.hasNumber\n                                        ? '編號 {number} 顯著有感地震'.i18n.args({\n                                            'number': report.number,\n                                          })\n                                        : '小區域有感地震'.i18n,\n                                    style: TextStyle(\n                                      color: context.colors.onSurfaceVariant,\n                                      fontSize: 14,\n                                    ),\n                                  ),\n                                  Text(\n                                    location,\n                                    style: const TextStyle(\n                                      fontSize: 20,\n                                      fontWeight: FontWeight.bold,\n                                    ),\n                                  ),\n                                ],\n                              ),\n                            ),\n                          ],\n                        ),\n                      ),\n                      Padding(\n                        padding: const .symmetric(\n                          horizontal: 24,\n                          vertical: 8,\n                        ),\n                        child: Wrap(\n                          spacing: 8,\n                          children: [\n                            ActionChip(\n                              avatar: Icon(\n                                Symbols.open_in_new_rounded,\n                                color: context.colors.onPrimary,\n                              ),\n                              label: Text('報告頁面'.i18n),\n                              backgroundColor: context.colors.primary,\n                              labelStyle: TextStyle(\n                                color: context.colors.onPrimary,\n                              ),\n                              side: BorderSide(color: context.colors.primary),\n                              onPressed: () {\n                                launchUrl(report.reportUrl);\n                              },\n                            ),\n                            ActionChip(\n                              avatar: const Icon(Symbols.replay_rounded),\n                              label: Text('重播'.i18n),\n                              onPressed: () {\n                                Navigator.push(\n                                  context,\n                                  MaterialPageRoute(\n                                    builder: (context) => MapMonitorPage(\n                                      replayTimestamp: report.time.millisecondsSinceEpoch - 2000,\n                                    ),\n                                  ),\n                                );\n                              },\n                            ),\n                          ],\n                        ),\n                      ),\n                      SegmentedList(\n                        label: Text('詳細資訊'),\n                        children: [\n                          SegmentedListTile(\n                            isFirst: true,\n                            label: Text('發震時間'.i18n),\n                            title: Text(\n                              DateFormat(\n                                'yyyy/MM/dd HH:mm:ss',\n                              ).format(report.time),\n                            ),\n                          ),\n                          SegmentedListTile(\n                            label: Text('位於'.i18n),\n                            title: Text(report.convertLatLon()),\n                          ),\n                          Row(\n                            spacing: 2,\n                            children: [\n                              Expanded(\n                                child: SegmentedListTile(\n                                  borderRadius: BorderRadius.only(\n                                    bottomLeft: .circular(16),\n                                  ),\n                                  label: Text('地震規模'.i18n),\n                                  title: Row(\n                                    children: [\n                                      Container(\n                                        height: 12,\n                                        width: 12,\n                                        margin: const .only(right: 6),\n                                        decoration: BoxDecoration(\n                                          borderRadius: .circular(10),\n                                          color: MagnitudeColor.magnitude(\n                                            report.magnitude,\n                                          ),\n                                        ),\n                                      ),\n                                      BodyText.large(\n                                        'M ${report.magnitude}',\n                                        weight: .bold,\n                                      ),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                              Expanded(\n                                child: SegmentedListTile(\n                                  borderRadius: BorderRadius.only(\n                                    bottomRight: .circular(16),\n                                  ),\n                                  label: Text('震源深度'.i18n),\n                                  title: Row(\n                                    children: [\n                                      Container(\n                                        height: 12,\n                                        width: 12,\n                                        margin: const .only(right: 6),\n                                        decoration: BoxDecoration(\n                                          borderRadius: .circular(10),\n                                          color: getDepthColor(report.depth),\n                                        ),\n                                      ),\n                                      Text('${report.depth} km'),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                            ],\n                          ),\n                        ],\n                      ),\n                      SegmentedList(\n                        label: Text('各地震度'.i18n),\n                        children: [\n                          for (final (\n                                index,\n                                MapEntry(key: areaName, value: area),\n                              )\n                              in report.list.entries.indexed)\n                            SegmentedListTile(\n                              isFirst: index == 0,\n                              isLast: index == report.list.length - 1,\n                              title: Text(areaName),\n                              content: Wrap(\n                                spacing: 8,\n                                runSpacing: 8,\n                                children: [\n                                  for (final MapEntry(\n                                        key: townName,\n                                        value: town,\n                                      )\n                                      in area.town.entries)\n                                    ActionChip(\n                                      padding: const EdgeInsets.all(\n                                        4,\n                                      ),\n                                      side: BorderSide(\n                                        color: IntensityColor.intensity(\n                                          town.intensity,\n                                        ),\n                                      ),\n                                      backgroundColor: IntensityColor.intensity(\n                                        town.intensity,\n                                      ).withValues(alpha: 0.16),\n                                      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n                                      avatar: AspectRatio(\n                                        aspectRatio: 1,\n                                        child: Container(\n                                          decoration: BoxDecoration(\n                                            borderRadius: .circular(6),\n                                            color: IntensityColor.intensity(\n                                              town.intensity,\n                                            ),\n                                          ),\n                                          child: Center(\n                                            child: Text(\n                                              town.intensity.asIntensityDisplayLabel,\n                                              style: TextStyle(\n                                                height: 1,\n                                                fontSize: 15,\n                                                fontWeight: FontWeight.bold,\n                                                color: IntensityColor.onIntensity(\n                                                  town.intensity,\n                                                ),\n                                              ),\n                                            ),\n                                          ),\n                                        ),\n                                      ),\n                                      label: Text(townName),\n                                      onPressed: () {\n                                        sheetController.collapse();\n                                        widget.manager.controller.animateCamera(\n                                          CameraUpdate.newLatLng(\n                                            LatLng(\n                                              town.lat,\n                                              town.lon,\n                                            ),\n                                          ),\n                                        );\n                                      },\n                                    ),\n                                ],\n                              ),\n                            ),\n                        ],\n                      ),\n                      SegmentedList(\n                        label: Text('地震報告圖'.i18n),\n                        children: [\n                          Padding(\n                            padding: const .symmetric(horizontal: 8),\n                            child: EnlargeableImage(\n                              aspectRatio: 4 / 3,\n                              heroTag: 'report-image-${report.id}',\n                              imageUrl: report.reportImageUrl,\n                              imageName: report.reportImageName,\n                            ),\n                          ),\n                        ],\n                      ),\n                      if (report.hasNumber && report.intensityMapImageUrl != null)\n                        SegmentedList(\n                          label: Text('震度圖'.i18n),\n                          children: [\n                            Padding(\n                              padding: const .symmetric(\n                                horizontal: 8,\n                              ),\n                              child: SafeImageSection(\n                                builder: (onError) => EnlargeableImage(\n                                  aspectRatio: 2334 / 2977,\n                                  heroTag: 'intensity-image-${report.id}',\n                                  imageUrl: report.intensityMapImageUrl!,\n                                  imageName: report.intensityMapImageName!,\n                                  onLoadFailed: onError,\n                                ),\n                              ),\n                            ),\n                          ],\n                        ),\n                      if (report.hasNumber && report.pgaMapImageUrl != null)\n                        SegmentedList(\n                          label: Text('最大地動加速度圖'.i18n),\n                          children: [\n                            Padding(\n                              padding: const .symmetric(\n                                horizontal: 8,\n                              ),\n                              child: SafeImageSection(\n                                builder: (onError) => EnlargeableImage(\n                                  aspectRatio: 2334 / 2977,\n                                  heroTag: 'pga-image-${report.id}',\n                                  imageUrl: report.pgaMapImageUrl!,\n                                  imageName: report.pgaMapImageName!,\n                                  onLoadFailed: onError,\n                                ),\n                              ),\n                            ),\n                          ],\n                        ),\n                      if (report.hasNumber && report.pgvMapImageUrl != null)\n                        SegmentedList(\n                          label: Text('最大地動速度圖'.i18n),\n                          children: [\n                            Padding(\n                              padding: const .symmetric(\n                                horizontal: 8,\n                              ),\n                              child: SafeImageSection(\n                                builder: (onError) => EnlargeableImage(\n                                  aspectRatio: 2334 / 2977,\n                                  heroTag: 'pgv-image-${report.id}',\n                                  imageUrl: report.pgvMapImageUrl!,\n                                  imageName: report.pgvMapImageName!,\n                                  onLoadFailed: onError,\n                                ),\n                              ),\n                            ),\n                          ],\n                        ),\n                    ];\n                  }\n\n                  if (widget.manager._shouldResetScroll) {\n                    widget.manager._shouldResetScroll = false;\n                    WidgetsBinding.instance.addPostFrameCallback((_) {\n                      if (controller.hasClients) {\n                        controller.jumpTo(0);\n                      }\n                    });\n                  }\n\n                  return CustomScrollView(\n                    controller: controller,\n                    slivers: [\n                      SliverAppBar(\n                        title: Text('地震報告'.i18n),\n                        leading: BackButton(\n                          onPressed: () {\n                            widget.manager.shouldExpandOnReturn.value = true;\n                            widget.manager.setReport(null);\n                          },\n                        ),\n                        floating: true,\n                        snap: true,\n                        pinned: true,\n                      ),\n                      SliverList.list(children: content),\n                      SliverPadding(\n                        padding: .only(\n                          bottom: context.padding.bottom + 16,\n                        ),\n                      ),\n                    ],\n                  );\n                },\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/temperature.dart",
    "content": "/// Map layer manager and associated UI for temperature data.\nlibrary;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// Intermediate data object for a single weather-station temperature reading.\nclass TemperatureData {\n  /// The station's latitude in decimal degrees.\n  final double latitude;\n\n  /// The station's longitude in decimal degrees.\n  final double longitude;\n\n  /// The measured air temperature in degrees Celsius.\n  final double temperature;\n\n  /// The human-readable name of the weather station.\n  final String stationName;\n\n  /// The county in which the station is located.\n  final String county;\n\n  /// The town in which the station is located.\n  final String town;\n\n  /// The station's unique identifier.\n  final String id;\n\n  /// Creates a [TemperatureData] with all required fields.\n  TemperatureData({\n    required this.latitude,\n    required this.longitude,\n    required this.temperature,\n    required this.stationName,\n    required this.county,\n    required this.town,\n    required this.id,\n  });\n}\n\n/// Manages the temperature overlay layer on the DPIP map.\nclass TemperatureMapLayerManager extends MapLayerManager {\n  /// Creates a [TemperatureMapLayerManager] bound to [context] and\n  /// [controller].\n  TemperatureMapLayerManager(super.context, super.controller);\n\n  /// Vertical offset of the first text label line above the station circle.\n  static const double kLabelBaseOffset = 1.0;\n\n  /// Vertical spacing between consecutive text label lines.\n  static const double kLabelLineHeight = 1.1;\n\n  /// The currently displayed temperature observation time string.\n  final currentTemperatureTime = ValueNotifier<String?>(\n    GlobalProviders.data.temperature.firstOrNull,\n  );\n\n  /// Whether a time-change operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  DateTime? _lastFetchTime;\n\n  /// Called with the new time string whenever the displayed time changes.\n  Function(String)? onTimeChanged;\n\n  /// Switches the displayed temperature data to [time].\n  ///\n  /// Does nothing if [time] is already current or a load is in progress.\n  Future<void> setTemperatureTime(String time) async {\n    if (currentTemperatureTime.value == time || isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      await remove();\n      currentTemperatureTime.value = time;\n      await setup();\n\n      onTimeChanged?.call(time);\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'TemperatureMapLayerManager.setTemperatureTime',\n        e,\n        s,\n      );\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future<void> _focus() async {\n    try {\n      final location = GlobalProviders.location.coordinates;\n\n      if (location != null && location.isValid) {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(location, 7.4),\n        );\n      } else {\n        await controller.animateCamera(\n          CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n        );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('TemperatureMapLayerManager._focus', e, s);\n    }\n  }\n\n  Future<void> _fetchData() async {\n    try {\n      final temperatureList = (await ExpTech().getWeatherList()).reversed.toList();\n      if (!context.mounted) return;\n\n      GlobalProviders.data.setTemperature(temperatureList);\n      currentTemperatureTime.value ??= temperatureList.first;\n      _lastFetchTime = DateTime.now();\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'TemperatureMapLayerManager._fetchData',\n        e,\n        s,\n      );\n    }\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    final colors = context.colors;\n\n    try {\n      if (GlobalProviders.data.temperature.isEmpty) await _fetchData();\n\n      final time = currentTemperatureTime.value;\n\n      if (time == null) throw Exception('Time is null');\n\n      final sourceId = MapSourceIds.temperature(time);\n      final layerId = MapLayerIds.temperature(time);\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (!isSourceExists) {\n        late final List<WeatherStation> weatherData;\n\n        if (GlobalProviders.data.weatherData.containsKey(time)) {\n          weatherData = GlobalProviders.data.weatherData[time]!;\n        } else {\n          weatherData = await ExpTech().getWeather(time);\n          GlobalProviders.data.setWeatherData(time, weatherData);\n        }\n\n        final features = weatherData\n            .where((station) => station.data.air.temperature != -99)\n            .map((station) => station.toFeatureBuilder())\n            .toList();\n\n        final data = GeoJsonBuilder().setFeatures(features).build();\n\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        // circles\n        final properties = CircleLayerProperties(\n          circleColor: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.get, 'temperature'],\n            -10,\n            '#4d4e51',\n            0,\n            '#0000ff',\n            10,\n            '#75c16f',\n            20,\n            '#f6e78b',\n            30,\n            '#ff4500',\n            40,\n            '#7e2899',\n          ],\n          circleRadius: kCircleIconSize,\n          circleOpacity: 0.75,\n          circleStrokeColor: colors.outlineVariant.toHexStringRGB(),\n          circleStrokeWidth: 0.5,\n          circleStrokeOpacity: 0.75,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        // labels\n        final temperature = [\n          Expressions.caseExpression,\n          GlobalProviders.ui.useFahrenheit,\n          [\n            Expressions.round,\n            [\n              Expressions.plus,\n              [\n                Expressions.multiply,\n                [Expressions.get, 'temperature'],\n                1.8,\n              ],\n              32,\n            ],\n          ],\n          [Expressions.get, 'temperature'],\n        ];\n        final labelNameProps = SymbolLayerProperties(\n          textField: [Expressions.get, 'name'],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [0, kLabelBaseOffset],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelValueProps = SymbolLayerProperties(\n          textField: [\n            Expressions.concat,\n            temperature,\n            if (GlobalProviders.ui.useFahrenheit) '℉' else '℃',\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 1],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n        await controller.addLayer(\n          sourceId,\n          '$layerId-label-name',\n          labelNameProps,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          minzoom: 10,\n        );\n        await controller.addLayer(\n          sourceId,\n          '$layerId-label-value',\n          labelValueProps,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          minzoom: 10,\n        );\n      }\n\n      if (isSourceExists && isLayerExists) return;\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('TemperatureMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    final layerId = MapLayerIds.temperature(currentTemperatureTime.value);\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n      await controller.setLayerVisibility('$layerId-label-name', false);\n      await controller.setLayerVisibility('$layerId-label-value', false);\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('TemperatureMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final layerId = MapLayerIds.temperature(currentTemperatureTime.value);\n\n    try {\n      await controller.setLayerVisibility(layerId, true);\n      await controller.setLayerVisibility('$layerId-label-name', true);\n      await controller.setLayerVisibility('$layerId-label-value', true);\n\n      await _focus();\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5)\n        await _fetchData();\n    } catch (e, s) {\n      TalkerManager.instance.error('TemperatureMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final layerId = MapLayerIds.temperature(currentTemperatureTime.value);\n      final sourceId = MapSourceIds.temperature(currentTemperatureTime.value);\n\n      await Future.wait([\n        controller.removeLayer(layerId),\n        controller.removeLayer('$layerId-label-name'),\n        controller.removeLayer('$layerId-label-value'),\n        controller.removeSource(sourceId),\n      ]);\n    } catch (e, s) {\n      TalkerManager.instance.error('TemperatureMapLayerManager.remove', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  Widget build(BuildContext context) => TemperatureMapLayerSheet(manager: this);\n}\n\n/// The bottom sheet and legend overlay for the temperature layer.\nclass TemperatureMapLayerSheet extends StatelessWidget {\n  /// The [TemperatureMapLayerManager] whose state drives this sheet.\n  final TemperatureMapLayerManager manager;\n\n  /// Creates a [TemperatureMapLayerSheet] for the given [manager].\n  const TemperatureMapLayerSheet({super.key, required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        MorphingSheet(\n          title: '氣溫'.i18n,\n          borderRadius: .circular(16),\n          elevation: 4,\n          partialBuilder: (context, controller, sheetController) {\n            return Padding(\n              padding: const .symmetric(vertical: 8),\n              child: Selector<DpipDataModel, UnmodifiableListView<String>>(\n                selector: (context, model) => model.temperature,\n                builder: (context, temperature, header) {\n                  final times = temperature.map((time) {\n                    final t = time.toSimpleDateTimeString().split(' ');\n                    return (date: t[0], time: t[1], value: time);\n                  });\n                  final grouped = times.groupListsBy((time) => time.date).entries.toList();\n\n                  return Column(\n                    mainAxisSize: .min,\n                    children: [\n                      header!,\n                      SizedBox(\n                        height: kMinInteractiveDimension,\n                        child: ValueListenableBuilder<String?>(\n                          valueListenable: manager.currentTemperatureTime,\n                          builder: (context, currentTemperatureTime, child) {\n                            return ListView.builder(\n                              padding: const .symmetric(\n                                horizontal: 16,\n                              ),\n                              scrollDirection: .horizontal,\n                              physics: const AlwaysScrollableScrollPhysics(),\n                              itemCount: grouped.length,\n                              itemBuilder: (context, index) {\n                                final MapEntry(key: date, value: group) = grouped[index];\n\n                                final children = <Widget>[Text(date)];\n\n                                for (final time in group) {\n                                  final isSelected = time.value == currentTemperatureTime;\n\n                                  children.add(\n                                    ValueListenableBuilder<bool>(\n                                      valueListenable: manager.isLoading,\n                                      builder: (context, isLoading, child) {\n                                        return FilterChip(\n                                          selected: isSelected,\n                                          showCheckmark: !isLoading,\n                                          label: Text(time.time),\n                                          side: BorderSide(\n                                            color: isSelected\n                                                ? context.colors.primary\n                                                : context.colors.outlineVariant,\n                                          ),\n                                          avatar: isSelected && isLoading\n                                              ? const LoadingIcon()\n                                              : null,\n                                          onSelected: isLoading\n                                              ? null\n                                              : (selected) {\n                                                  if (!selected) return;\n                                                  manager.setTemperatureTime(\n                                                    time.value,\n                                                  );\n                                                },\n                                        );\n                                      },\n                                    ),\n                                  );\n                                }\n\n                                children.add(\n                                  const Padding(\n                                    padding: .only(right: 8),\n                                    child: VerticalDivider(\n                                      width: 16,\n                                      indent: 8,\n                                      endIndent: 8,\n                                    ),\n                                  ),\n                                );\n\n                                return Row(\n                                  mainAxisSize: .min,\n                                  spacing: 8,\n                                  children: children,\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ),\n                    ],\n                  );\n                },\n                child: Padding(\n                  padding: const .symmetric(\n                    horizontal: 16,\n                    vertical: 8,\n                  ),\n                  child: Row(\n                    spacing: 8,\n                    children: [\n                      const Icon(Symbols.thermostat_rounded, size: 24),\n                      Text('氣溫'.i18n, style: context.texts.titleMedium),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n        Positioned(\n          top: 24 + 48 + 16,\n          left: 24,\n          child: SafeArea(\n            child: BlurredContainer(\n              elevation: 4,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n              child: Selector<SettingsUserInterfaceModel, bool>(\n                selector: (context, model) => model.useFahrenheit,\n                builder: (context, useFahrenheit, child) {\n                  return ColorLegend(\n                    reverse: true,\n                    unit: useFahrenheit ? '℉' : '℃',\n                    appendUnit: true,\n                    items: [\n                      ColorLegendItem(\n                        color: const Color(0xff4d4e51),\n                        value: useFahrenheit ? -10.asFahrenheit : -10,\n                      ),\n                      ColorLegendItem(\n                        color: const Color(0xff0000ff),\n                        value: useFahrenheit ? 0.asFahrenheit : 0,\n                      ),\n                      ColorLegendItem(\n                        color: const Color(0xff75c16f),\n                        value: useFahrenheit ? 10.asFahrenheit : 10,\n                      ),\n                      ColorLegendItem(\n                        color: const Color(0xfff6e78b),\n                        value: useFahrenheit ? 20.asFahrenheit : 20,\n                      ),\n                      ColorLegendItem(\n                        color: const Color(0xffff4500),\n                        value: useFahrenheit ? 30.asFahrenheit : 30,\n                      ),\n                      ColorLegendItem(\n                        color: const Color(0xff7e2899),\n                        value: useFahrenheit ? 40.asFahrenheit : 40,\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/tsunami.dart",
    "content": "/// Map layer manager and associated UI for tsunami warning data.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/tsunami/tsunami.dart';\nimport 'package:dpip/api/model/tsunami/tsunami_actual.dart';\nimport 'package:dpip/api/model/tsunami/tsunami_estimate.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app_old/page/map/tsunami/tsunami_estimate_list.dart';\nimport 'package:dpip/app_old/page/map/tsunami/tsunami_observed_list.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flex_color_picker/flex_color_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:intl/intl.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Manages the tsunami warning overlay layer on the DPIP map.\nclass TsunamiMapLayerManager extends MapLayerManager {\n  /// Creates a [TsunamiMapLayerManager] bound to [context] and [controller].\n  TsunamiMapLayerManager(super.context, super.controller);\n\n  /// The currently displayed tsunami data object, or `null` when none.\n  final currentTsunami = ValueNotifier(null);\n\n  /// Whether a data-load operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    try {\n      final sourceId = MapSourceIds.tsunami(currentTsunami.value);\n      final layerId = MapLayerIds.tsunami(currentTsunami.value);\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (isSourceExists && isLayerExists) return;\n\n      if (!isSourceExists) {\n        TalkerManager.instance.info('Added Source \"$sourceId\"');\n\n        if (!context.mounted) return;\n      }\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('TsunamiMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    final layerId = MapLayerIds.tsunami(currentTsunami.value);\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n      TalkerManager.instance.info('Hiding Layer \"$layerId\"');\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('TsunamiMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final layerId = MapLayerIds.tsunami(currentTsunami.value);\n\n    try {\n      await controller.setLayerVisibility(layerId, true);\n      TalkerManager.instance.info('Showing Layer \"$layerId\"');\n\n      visible = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('TsunamiMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final layerId = MapLayerIds.tsunami(currentTsunami.value);\n      final sourceId = MapSourceIds.tsunami(currentTsunami.value);\n\n      await controller.removeLayer(layerId);\n      TalkerManager.instance.info('Removed Layer \"$layerId\"');\n\n      await controller.removeSource(sourceId);\n      TalkerManager.instance.info('Removed Source \"$sourceId\"');\n    } catch (e, s) {\n      TalkerManager.instance.error('TsunamiMapLayerManager.dispose', e, s);\n    }\n    didSetup = false;\n  }\n\n  @override\n  Widget build(BuildContext context) => TsunamiMapLayerSheet(manager: this);\n}\n\n/// The full-screen map and scrollable sheet for the tsunami warning layer.\nclass TsunamiMapLayerSheet extends StatefulWidget {\n  /// The [TsunamiMapLayerManager] whose state drives this sheet.\n  final TsunamiMapLayerManager manager;\n\n  /// Creates a [TsunamiMapLayerSheet] for the given [manager].\n  const TsunamiMapLayerSheet({super.key, required this.manager});\n\n  @override\n  State<TsunamiMapLayerSheet> createState() => _TsunamiMapLayerSheetState();\n}\n\nclass _TsunamiMapLayerSheetState extends State<TsunamiMapLayerSheet> {\n  late MapLibreMapController _mapController;\n  Timer? _blinkTimer;\n  Tsunami? tsunami;\n  String tsunamiStatus = '';\n  int _isTsunamiVisible = 0;\n  String _tsunami_id = '';\n  int _tsunami_serial = 0;\n  String? _selectedOption;\n  double userLat = 0;\n  double userLon = 0;\n\n  Future<void> _initMap(MapLibreMapController controller) async {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    await refreshTsunami();\n    await _mapController.addSource(\n      'tsunami-data',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n\n    if (tsunami != null) {\n      await addTsunamiObservationPoints(tsunami!);\n    }\n\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    }\n    userLat = Global.preference.getDouble('user-lat') ?? 0.0;\n    userLon = Global.preference.getDouble('user-lon') ?? 0.0;\n\n    final location = LatLng(userLat, userLon);\n\n    if (location.isValid) {\n      await _addUserLocationMarker();\n    }\n\n    setState(() {});\n  }\n\n  String heightToColor(int height) {\n    Color color;\n    if (height == 3) {\n      color = const Color(0xFFE543FF);\n    } else if (height == 2) {\n      color = const Color(0xFFC90000);\n    } else if (height == 1) {\n      color = const Color(0xFFFFC900);\n    } else {\n      color = const Color(0xFF00AAFF);\n    }\n    return '#${color.hex}';\n  }\n\n  DateTime _convertTimestamp(int timestamp) {\n    return DateTime.fromMillisecondsSinceEpoch(timestamp);\n  }\n\n  Future<void> addTsunamiObservationPoints(Tsunami tsunami) async {\n    await _mapController.removeLayer('tsunami-actual-circles');\n    await _mapController.removeLayer('tsunami-actual-labels');\n    _blinkTimer?.cancel();\n    await _mapController.setLayerProperties(\n      'tsunami',\n      const LineLayerProperties(lineOpacity: 0),\n    );\n    if (tsunami.info.type == 'estimate') {\n      final Map<String, String> areaColor = {};\n      for (final station in tsunami.info.data) {\n        final estimateStation = station as TsunamiEstimate;\n        areaColor[estimateStation.area] = heightToColor(\n          estimateStation.waveHeight,\n        );\n      }\n\n      _blinkTimer = Timer.periodic(const Duration(milliseconds: 500), (\n        timer,\n      ) async {\n        if (!mounted) return;\n        await _mapController.setLayerProperties(\n          'tsunami',\n          LineLayerProperties(\n            lineColor: [\n              'match',\n              ['get', 'AREANAME'],\n              ...areaColor.entries.expand((entry) => [entry.key, entry.value]),\n              '#000000',\n            ],\n            lineOpacity: (_isTsunamiVisible < 6) ? 1 : 0,\n          ),\n        );\n        _isTsunamiVisible++;\n        if (_isTsunamiVisible >= 8) _isTsunamiVisible = 0;\n      });\n    } else {\n      final features = tsunami.info.data.map((station) {\n        final actualStation = station as TsunamiActual;\n        return {\n          'type': 'Feature',\n          'properties': {\n            'name': actualStation.name,\n            'id': actualStation.id,\n            'waveHeight': actualStation.waveHeight,\n            'arrivalTime': DateFormat(\n              'MM/dd HH:mm',\n            ).format(_convertTimestamp(actualStation.arrivalTime)),\n          },\n          'geometry': {\n            'type': 'Point',\n            'coordinates': [actualStation.lon ?? 0, actualStation.lat ?? 0],\n          },\n        };\n      }).toList();\n\n      await _mapController.setGeoJsonSource('tsunami-data', {\n        'type': 'FeatureCollection',\n        'features': features,\n      });\n\n      await _mapController.addLayer(\n        'tsunami-data',\n        'tsunami-actual-circles',\n        const CircleLayerProperties(\n          circleRadius: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            7,\n            8,\n            12,\n            18,\n          ],\n          circleColor: [\n            Expressions.step,\n            [Expressions.get, 'waveHeight'],\n            '#00AAFF',\n            30,\n            '#FFC900',\n            100,\n            '#C90000',\n            300,\n            '#E543FF',\n          ],\n          circleOpacity: 1,\n          circleStrokeWidth: 0.2,\n          circleStrokeColor: '#000000',\n          circleStrokeOpacity: 0.7,\n        ),\n      );\n\n      await _mapController.addSymbolLayer(\n        'tsunami-data',\n        'tsunami-actual-labels',\n        const SymbolLayerProperties(\n          textField: [\n            Expressions.concat,\n            ['get', 'name'],\n            '\\n',\n            ['get', 'arrivalTime'],\n            '\\n',\n            ['get', 'waveHeight'],\n            'cm\\n抵達',\n          ],\n          textSize: 12,\n          textColor: '#ffffff',\n          textHaloColor: '#000000',\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [\n            Expressions.literal,\n            [0, 3.5],\n          ],\n        ),\n        minzoom: 7,\n      );\n    }\n  }\n\n  Future<void> _addUserLocationMarker() async {\n    await _mapController.addSource(\n      'markers-geojson',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n    await _mapController.addLayer(\n      'markers-geojson',\n      'markers',\n      const SymbolLayerProperties(\n        symbolZOrder: 'source',\n        iconSize: [\n          Expressions.interpolate,\n          ['linear'],\n          [Expressions.zoom],\n          5,\n          0.5,\n          10,\n          1.5,\n        ],\n        iconImage: [\n          Expressions.match,\n          [Expressions.get, 'cross'],\n          1,\n          'cross',\n          'gps',\n        ],\n        iconAllowOverlap: true,\n        iconIgnorePlacement: true,\n      ),\n    );\n    final List markersFeatures = [];\n    final tsunami = this.tsunami;\n    if (tsunami != null) {\n      markersFeatures.add({\n        'type': 'Feature',\n        'properties': {'cross': 1},\n        'geometry': {\n          'coordinates': [tsunami.eq.lon, tsunami.eq.lat],\n          'type': 'Point',\n        },\n      });\n    }\n    markersFeatures.add({\n      'type': 'Feature',\n      'properties': {},\n      'geometry': {\n        'coordinates': [userLon, userLat],\n        'type': 'Point',\n      },\n    });\n    await _mapController.setGeoJsonSource('markers-geojson', {\n      'type': 'FeatureCollection',\n      'features': markersFeatures,\n    });\n  }\n\n  Future<Tsunami?> refreshTsunami() async {\n    final idList = await ExpTech().getTsunamiList();\n    var id = '';\n    if (idList.isNotEmpty) {\n      id = idList.first;\n      _tsunami_id = id.split('-')[0];\n      _tsunami_serial = int.parse(id.split('-')[1]);\n      tsunami = await ExpTech().getTsunami(id);\n      (tsunami?.status == 0)\n          ? tsunamiStatus = '發布'\n          : (tsunami?.status == 1)\n          ? tsunamiStatus = '更新'\n          : tsunamiStatus = '解除';\n\n      final List<String> options = generateTsunamiOptions();\n      if (options.isNotEmpty && _selectedOption == null) {\n        _selectedOption = options.last;\n      }\n    }\n    return tsunami;\n  }\n\n  String getTime() {\n    final DateTime now = DateTime.now();\n    final DateFormat formatter = DateFormat('yyyy/MM/dd HH:mm');\n    final String formattedDate = formatter.format(now);\n    return formattedDate;\n  }\n\n  String convertLatLon(double latitude, double longitude) {\n    double lat = latitude;\n    final double lon = longitude;\n\n    var latFormat = '';\n    var lonFormat = '';\n\n    if (latitude > 90) lat = latitude - 180;\n    if (longitude > 180) lat = latitude - 360;\n\n    if (lat < 0) {\n      latFormat = '南緯 ${lat.abs} 度';\n    } else {\n      latFormat = '北緯 $lat 度';\n    }\n\n    if (lon < 0) {\n      lonFormat = '西經 ${lon.abs} 度';\n    } else {\n      lonFormat = '東經 $lon 度';\n    }\n\n    return '$latFormat　$lonFormat';\n  }\n\n  @override\n  void dispose() {\n    _blinkTimer?.cancel();\n    _mapController.dispose();\n    super.dispose();\n  }\n\n  List<String> generateTsunamiOptions() {\n    final List<String> options = [];\n    for (int i = 1; i <= _tsunami_serial; i++) {\n      options.add('$_tsunami_id-$i');\n    }\n    return options;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const sheetInitialSize = 0.16;\n    final List<String> tsunamiOptions = generateTsunamiOptions();\n\n    return Stack(\n      children: [\n        DpipMap(\n          onMapCreated: _initMap,\n          onStyleLoadedCallback: _loadMap,\n          minMaxZoomPreference: const MinMaxZoomPreference(3, 12),\n        ),\n        Positioned.fill(\n          child: DraggableScrollableSheet(\n            initialChildSize: sheetInitialSize,\n            minChildSize: sheetInitialSize,\n            snap: true,\n            builder: (context, scrollController) {\n              return ColoredBox(\n                color: context.colors.surface.withValues(alpha: 0.9),\n                child: ListView(\n                  controller: scrollController,\n                  children: [\n                    SizedBox(\n                      height: 24,\n                      child: Center(\n                        child: Container(\n                          width: 32,\n                          height: 4,\n                          decoration: BoxDecoration(\n                            color: context.colors.onSurfaceVariant.withValues(\n                              alpha: 0.4,\n                            ),\n                            borderRadius: .circular(16),\n                          ),\n                        ),\n                      ),\n                    ),\n                    Padding(\n                      padding: const .only(left: 20, right: 20),\n                      child: tsunami == null\n                          ? Container()\n                          : Column(\n                              crossAxisAlignment: .start,\n                              children: [\n                                Row(\n                                  crossAxisAlignment: .start,\n                                  children: [\n                                    Expanded(\n                                      child: Column(\n                                        crossAxisAlignment: CrossAxisAlignment.start,\n                                        children: [\n                                          Text(\n                                            tsunami == null ? '近期無海嘯資訊'.i18n : '海嘯警報'.i18n,\n                                            style: TextStyle(\n                                              fontSize: 28,\n                                              fontWeight: .bold,\n                                              color: context.colors.onSurface,\n                                            ),\n                                          ),\n                                          const SizedBox(height: 8),\n                                          if (tsunami != null)\n                                            Text(\n                                              '{id}號 第{serial}報'.i18n.args({\n                                                'id': tsunami!.id,\n                                                'serial': tsunami!.serial,\n                                              }),\n                                              style: TextStyle(\n                                                fontSize: 16,\n                                                fontWeight: .w500,\n                                                color: context.colors.onSurface.withValues(\n                                                  alpha: 0.8,\n                                                ),\n                                              ),\n                                            ),\n                                          const SizedBox(height: 4),\n                                          Text(\n                                            tsunami != null\n                                                ? '${tsunami!.time.toLocaleDateTimeString(context)} $tsunamiStatus'\n                                                : '${getTime()} 更新'.i18n,\n                                            style: TextStyle(\n                                              fontSize: 14,\n                                              color: context.colors.onSurfaceVariant,\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                    ),\n                                    const SizedBox(width: 16),\n                                    if (tsunamiOptions.isNotEmpty)\n                                      Container(\n                                        padding: const .symmetric(\n                                          horizontal: 12,\n                                          vertical: 4,\n                                        ),\n                                        decoration: BoxDecoration(\n                                          borderRadius: .circular(\n                                            8,\n                                          ),\n                                          color: context.colors.surface,\n                                          boxShadow: [\n                                            BoxShadow(\n                                              color: context.colors.onSurface.withValues(\n                                                alpha: 0.1,\n                                              ),\n                                              blurRadius: 4,\n                                              offset: const Offset(0, 2),\n                                            ),\n                                          ],\n                                        ),\n                                        child: DropdownButton<String>(\n                                          value: _selectedOption,\n                                          onChanged: (String? newValue) async {\n                                            if (newValue == null) return;\n                                            _selectedOption = newValue;\n                                            tsunami = await ExpTech().getTsunami(newValue);\n                                            tsunamiStatus = tsunami?.status == 0\n                                                ? '發布'.i18n\n                                                : tsunami?.status == 1\n                                                ? '更新'.i18n\n                                                : '解除'.i18n;\n                                            if (tsunami != null) {\n                                              await addTsunamiObservationPoints(\n                                                tsunami!,\n                                              );\n                                            }\n                                            setState(() {});\n                                          },\n                                          items: tsunamiOptions.reversed\n                                              .map<DropdownMenuItem<String>>((\n                                                String value,\n                                              ) {\n                                                return DropdownMenuItem<String>(\n                                                  value: value,\n                                                  child: Text(value),\n                                                );\n                                              })\n                                              .toList(),\n                                          style: TextStyle(\n                                            color: context.colors.onSurface,\n                                            fontSize: 16,\n                                            fontWeight: .w500,\n                                          ),\n                                          icon: Icon(\n                                            Icons.arrow_drop_down,\n                                            color: context.colors.onSurface,\n                                          ),\n                                          underline: const SizedBox(),\n                                          dropdownColor: context.colors.surface,\n                                        ),\n                                      ),\n                                  ],\n                                ),\n                                const SizedBox(height: 30),\n                                if (tsunami != null)\n                                  Column(\n                                    crossAxisAlignment: CrossAxisAlignment.start,\n                                    children: [\n                                      Text(\n                                        '${tsunami?.content}',\n                                        style: TextStyle(\n                                          fontSize: 18,\n                                          color: context.colors.onSurface,\n                                        ),\n                                      ),\n                                      const SizedBox(height: 20),\n                                      if (tsunami?.info.type == 'estimate')\n                                        Column(\n                                          crossAxisAlignment: CrossAxisAlignment.start,\n                                          children: [\n                                            Text(\n                                              '預估海嘯到達時間及波高'.i18n,\n                                              style: TextStyle(\n                                                fontSize: 22,\n                                                fontWeight: .bold,\n                                                color: context.colors.onSurface,\n                                              ),\n                                            ),\n                                            const SizedBox(height: 10),\n                                            TsunamiEstimateList(\n                                              tsunamiList: tsunami!.info.data,\n                                            ),\n                                          ],\n                                        )\n                                      else\n                                        Column(\n                                          crossAxisAlignment: CrossAxisAlignment.start,\n                                          children: [\n                                            Text(\n                                              '各地觀測到的海嘯'.i18n,\n                                              style: TextStyle(\n                                                fontSize: 22,\n                                                fontWeight: .bold,\n                                                color: context.colors.onSurface,\n                                              ),\n                                            ),\n                                            const SizedBox(height: 10),\n                                            TsunamiObservedList(\n                                              tsunamiList: tsunami!.info.data,\n                                            ),\n                                          ],\n                                        ),\n                                      const SizedBox(height: 15),\n                                      Text(\n                                        '地震資訊'.i18n,\n                                        style: TextStyle(\n                                          fontSize: 22,\n                                          fontWeight: .bold,\n                                          color: context.colors.onSurface,\n                                        ),\n                                      ),\n                                      const SizedBox(height: 10),\n                                      Row(\n                                        mainAxisAlignment: .spaceBetween,\n                                        children: [\n                                          Text(\n                                            '發生時間'.i18n,\n                                            style: TextStyle(\n                                              fontSize: 18,\n                                              color: context.colors.onSurface,\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                      Row(\n                                        mainAxisAlignment: .spaceBetween,\n                                        children: [\n                                          Text(\n                                            tsunami!.eq.time.toLocaleDateTimeString(\n                                              context,\n                                            ),\n                                            style: TextStyle(\n                                              fontSize: 18,\n                                              fontWeight: .bold,\n                                              color: context.colors.onSurface,\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                      const SizedBox(height: 4),\n                                      Row(\n                                        mainAxisAlignment: .spaceBetween,\n                                        children: [\n                                          Text(\n                                            '位於'.i18n,\n                                            style: TextStyle(\n                                              fontSize: 18,\n                                              color: context.colors.onSurface,\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                      Row(\n                                        mainAxisAlignment: .spaceBetween,\n                                        children: [\n                                          Column(\n                                            crossAxisAlignment: CrossAxisAlignment.start,\n                                            children: [\n                                              Text(\n                                                tsunami!.eq.loc,\n                                                style: TextStyle(\n                                                  fontSize: 18,\n                                                  fontWeight: .bold,\n                                                  color: context.colors.onSurface,\n                                                ),\n                                              ),\n                                              Text(\n                                                convertLatLon(\n                                                  tsunami!.eq.lat,\n                                                  tsunami!.eq.lon,\n                                                ),\n                                                style: TextStyle(\n                                                  fontSize: 14,\n                                                  fontWeight: .bold,\n                                                  color: context.colors.onSurface,\n                                                ),\n                                              ),\n                                            ],\n                                          ),\n                                        ],\n                                      ),\n                                      const SizedBox(height: 4),\n                                      Row(\n                                        children: [\n                                          Expanded(\n                                            child: Row(\n                                              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                                              children: [\n                                                Text(\n                                                  '規模'.i18n,\n                                                  style: TextStyle(\n                                                    fontSize: 18,\n                                                    color: context.colors.onSurface,\n                                                  ),\n                                                ),\n                                                Text(\n                                                  '${tsunami!.eq.mag}',\n                                                  style: TextStyle(\n                                                    fontSize: 18,\n                                                    fontWeight: .bold,\n                                                    color: context.colors.onSurface,\n                                                  ),\n                                                ),\n                                              ],\n                                            ),\n                                          ),\n                                          const SizedBox(width: 10),\n                                          Expanded(\n                                            child: Row(\n                                              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                                              children: [\n                                                Text(\n                                                  '深度'.i18n,\n                                                  style: TextStyle(\n                                                    fontSize: 18,\n                                                    color: context.colors.onSurface,\n                                                  ),\n                                                ),\n                                                Text(\n                                                  '${tsunami!.eq.depth}km',\n                                                  style: TextStyle(\n                                                    fontSize: 18,\n                                                    fontWeight: .bold,\n                                                    color: context.colors.onSurface,\n                                                  ),\n                                                ),\n                                              ],\n                                            ),\n                                          ),\n                                        ],\n                                      ),\n                                    ],\n                                  )\n                                else\n                                  Container(),\n                              ],\n                            ),\n                    ),\n                  ],\n                ),\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/managers/wind.dart",
    "content": "/// Map layer manager and associated UI for wind direction and speed data.\nlibrary;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/data.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/blurred_container.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// Intermediate data object for a single weather-station wind observation.\nclass WindData {\n  /// The station's latitude in decimal degrees.\n  final double latitude;\n\n  /// The station's longitude in decimal degrees.\n  final double longitude;\n\n  /// Wind direction in degrees clockwise from north.\n  final int direction;\n\n  /// Wind speed in metres per second.\n  final double speed;\n\n  /// The station's unique identifier.\n  final String id;\n\n  /// Creates a [WindData] with all required fields.\n  WindData({\n    required this.latitude,\n    required this.longitude,\n    required this.direction,\n    required this.speed,\n    required this.id,\n  });\n}\n\n/// Manages the wind direction and speed overlay layer on the DPIP map.\nclass WindMapLayerManager extends MapLayerManager {\n  /// Creates a [WindMapLayerManager] bound to [context] and [controller].\n  WindMapLayerManager(super.context, super.controller);\n\n  /// Vertical offset of the first text label line above the station arrow.\n  static const double kLabelBaseOffset = 2.0;\n\n  /// Vertical spacing between consecutive text label lines.\n  static const double kLabelLineHeight = 1.1;\n\n  /// The currently displayed wind observation time string.\n  final currentWindTime = ValueNotifier<String?>(\n    GlobalProviders.data.wind.firstOrNull,\n  );\n\n  /// Whether a time-change operation is currently in progress.\n  final isLoading = ValueNotifier<bool>(false);\n\n  DateTime? _lastFetchTime;\n\n  /// Called with the new time string whenever the displayed time changes.\n  Function(String)? onTimeChanged;\n\n  /// Switches the displayed wind data to [time].\n  ///\n  /// Does nothing if [time] is already current or a load is in progress.\n  Future<void> setWindTime(String time) async {\n    if (currentWindTime.value == time || isLoading.value) return;\n\n    isLoading.value = true;\n\n    try {\n      await remove();\n      currentWindTime.value = time;\n      await setup();\n\n      onTimeChanged?.call(time);\n    } catch (e, s) {\n      TalkerManager.instance.error('WindMapLayerManager.setWindTime', e, s);\n    } finally {\n      isLoading.value = false;\n    }\n  }\n\n  Future<void> _fetchData() async {\n    final windList = (await ExpTech().getWeatherList()).reversed.toList();\n    if (!context.mounted) return;\n\n    GlobalProviders.data.setWind(windList);\n    currentWindTime.value ??= windList.first;\n    _lastFetchTime = DateTime.now();\n  }\n\n  @override\n  Future<void> setup() async {\n    if (didSetup) return;\n\n    final colors = context.colors;\n\n    try {\n      if (GlobalProviders.data.wind.isEmpty) await _fetchData();\n\n      final time = currentWindTime.value;\n\n      if (time == null) throw Exception('Time is null');\n\n      final sourceId = MapSourceIds.wind(time);\n      final layerId = MapLayerIds.wind(time);\n\n      final isSourceExists = (await controller.getSourceIds()).contains(\n        sourceId,\n      );\n      final isLayerExists = (await controller.getLayerIds()).contains(layerId);\n\n      if (!isSourceExists) {\n        late final List<WeatherStation> weatherData;\n\n        if (GlobalProviders.data.weatherData.containsKey(time)) {\n          weatherData = GlobalProviders.data.weatherData[time]!;\n        } else {\n          weatherData = await ExpTech().getWeather(time);\n          GlobalProviders.data.setWeatherData(time, weatherData);\n        }\n\n        final features = weatherData\n            .where(\n              (station) => station.data.wind.direction != -99 && station.data.wind.speed != -99,\n            )\n            .map((station) => station.toFeatureBuilder())\n            .toList();\n\n        final data = GeoJsonBuilder().setFeatures(features).build();\n\n        final properties = GeojsonSourceProperties(data: data);\n\n        await controller.addSource(sourceId, properties);\n\n        if (!context.mounted) return;\n      }\n\n      if (!isLayerExists) {\n        // arrows\n        final properties = SymbolLayerProperties(\n          iconImage: [Expressions.get, 'icon'],\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.1,\n            15,\n            0.8,\n          ],\n          iconRotate: [Expressions.get, 'wind_direction'],\n          iconOpacity: 0.75,\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelNameProps = SymbolLayerProperties(\n          textField: [Expressions.get, 'name'],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [0, kLabelBaseOffset],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        final labelValueProps = SymbolLayerProperties(\n          textField: [\n            Expressions.concat,\n            [Expressions.get, 'wind_speed'],\n            'm/s',\n          ],\n          textSize: 10,\n          textColor: colors.onSurfaceVariant.toHexStringRGB(),\n          textHaloColor: colors.outlineVariant.toHexStringRGB(),\n          textHaloWidth: 1,\n          textFont: ['Noto Sans TC Bold'],\n          textOffset: [0, kLabelBaseOffset + kLabelLineHeight * 1],\n          textAnchor: 'top',\n          textAllowOverlap: true,\n          textIgnorePlacement: true,\n          visibility: visible ? 'visible' : 'none',\n        );\n\n        await controller.addLayer(\n          sourceId,\n          layerId,\n          properties,\n          belowLayerId: BaseMapLayerIds.userLocation,\n        );\n        await controller.addLayer(\n          sourceId,\n          '$layerId-label-name',\n          labelNameProps,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          minzoom: 10,\n        );\n        await controller.addLayer(\n          sourceId,\n          '$layerId-label-value',\n          labelValueProps,\n          belowLayerId: BaseMapLayerIds.userLocation,\n          minzoom: 10,\n        );\n      }\n\n      if (isSourceExists && isLayerExists) return;\n\n      didSetup = true;\n    } catch (e, s) {\n      TalkerManager.instance.error('WindMapLayerManager.setup', e, s);\n    }\n  }\n\n  @override\n  Future<void> hide() async {\n    if (!visible) return;\n\n    final layerId = MapLayerIds.wind(currentWindTime.value);\n\n    final nameLayerId = '$layerId-label-name';\n    final valueLayerId = '$layerId-label-value';\n\n    try {\n      await controller.setLayerVisibility(layerId, false);\n      await controller.setLayerVisibility(nameLayerId, false);\n      await controller.setLayerVisibility(valueLayerId, false);\n\n      visible = false;\n    } catch (e, s) {\n      TalkerManager.instance.error('WindMapLayerManager.hide', e, s);\n    }\n  }\n\n  @override\n  Future<void> show() async {\n    if (visible) return;\n\n    final layerId = MapLayerIds.wind(currentWindTime.value);\n\n    final nameLayerId = '$layerId-label-name';\n    final valueLayerId = '$layerId-label-value';\n\n    try {\n      await controller.setLayerVisibility(layerId, true);\n      await controller.setLayerVisibility(nameLayerId, true);\n      await controller.setLayerVisibility(valueLayerId, true);\n\n      visible = true;\n\n      if (_lastFetchTime == null || DateTime.now().difference(_lastFetchTime!).inMinutes > 5)\n        await _fetchData();\n    } catch (e, s) {\n      TalkerManager.instance.error('WindMapLayerManager.show', e, s);\n    }\n  }\n\n  @override\n  Future<void> remove() async {\n    try {\n      final layerId = MapLayerIds.wind(currentWindTime.value);\n      final sourceId = MapSourceIds.wind(currentWindTime.value);\n\n      await Future.wait([\n        controller.removeLayer(layerId),\n        controller.removeLayer('$layerId-label-name'),\n        controller.removeLayer('$layerId-label-value'),\n        controller.removeSource(sourceId),\n      ]);\n    } catch (e, s) {\n      TalkerManager.instance.error('WindMapLayerManager.dispose', e, s);\n    }\n\n    didSetup = false;\n  }\n\n  @override\n  Widget build(BuildContext context) => WindMapLayerSheet(manager: this);\n}\n\n/// The bottom sheet and legend overlay for the wind layer.\nclass WindMapLayerSheet extends StatelessWidget {\n  /// The [WindMapLayerManager] whose state drives this sheet.\n  final WindMapLayerManager manager;\n\n  /// Creates a [WindMapLayerSheet] for the given [manager].\n  const WindMapLayerSheet({super.key, required this.manager});\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        MorphingSheet(\n          title: '風向/風速'.i18n,\n          borderRadius: .circular(16),\n          elevation: 4,\n          partialBuilder: (context, controller, sheetController) {\n            return Padding(\n              padding: const .symmetric(vertical: 8),\n              child: Selector<DpipDataModel, UnmodifiableListView<String>>(\n                selector: (context, model) => model.wind,\n                builder: (context, wind, child) {\n                  final times = wind.map((time) {\n                    final t = time.toSimpleDateTimeString().split(' ');\n                    return (date: t[0], time: t[1], value: time);\n                  });\n                  final grouped = times.groupListsBy((time) => time.date).entries.toList();\n\n                  return Column(\n                    mainAxisSize: .min,\n                    children: [\n                      Padding(\n                        padding: const .symmetric(\n                          horizontal: 16,\n                          vertical: 8,\n                        ),\n                        child: Row(\n                          spacing: 8,\n                          children: [\n                            const Icon(Symbols.wind_power_rounded, size: 24),\n                            Text(\n                              '風向/風速'.i18n,\n                              style: context.texts.titleMedium,\n                            ),\n                          ],\n                        ),\n                      ),\n                      SizedBox(\n                        height: kMinInteractiveDimension,\n                        child: ValueListenableBuilder<String?>(\n                          valueListenable: manager.currentWindTime,\n                          builder: (context, currentWindTime, child) {\n                            return ListView.builder(\n                              padding: const .symmetric(\n                                horizontal: 16,\n                              ),\n                              scrollDirection: .horizontal,\n                              physics: const AlwaysScrollableScrollPhysics(),\n                              itemCount: grouped.length,\n                              itemBuilder: (context, index) {\n                                final MapEntry(key: date, value: group) = grouped[index];\n\n                                final children = <Widget>[Text(date)];\n\n                                for (final time in group) {\n                                  final isSelected = time.value == currentWindTime;\n\n                                  children.add(\n                                    ValueListenableBuilder<bool>(\n                                      valueListenable: manager.isLoading,\n                                      builder: (context, isLoading, child) {\n                                        return FilterChip(\n                                          selected: isSelected,\n                                          showCheckmark: !isLoading,\n                                          label: Text(time.time),\n                                          side: BorderSide(\n                                            color: isSelected\n                                                ? context.colors.primary\n                                                : context.colors.outlineVariant,\n                                          ),\n                                          avatar: isSelected && isLoading\n                                              ? const LoadingIcon()\n                                              : null,\n                                          onSelected: isLoading\n                                              ? null\n                                              : (selected) {\n                                                  if (!selected) return;\n                                                  manager.setWindTime(\n                                                    time.value,\n                                                  );\n                                                },\n                                        );\n                                      },\n                                    ),\n                                  );\n                                }\n\n                                children.add(\n                                  const Padding(\n                                    padding: .only(right: 8),\n                                    child: VerticalDivider(\n                                      width: 16,\n                                      indent: 8,\n                                      endIndent: 8,\n                                    ),\n                                  ),\n                                );\n\n                                return Row(\n                                  mainAxisSize: .min,\n                                  spacing: 8,\n                                  children: children,\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ),\n                    ],\n                  );\n                },\n              ),\n            );\n          },\n        ),\n        Positioned(\n          top: 24 + 48 + 16,\n          left: 24,\n          child: SafeArea(\n            child: BlurredContainer(\n              elevation: 4,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.4),\n              child: Legend(\n                unit: 'm/s',\n                items: [\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.navigation_rounded,\n                      fill: Color(0xffff006b),\n                      size: 20,\n                    ),\n                    label: '≥ 32.7',\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.navigation_rounded,\n                      fill: Color(0xff8000ff),\n                      size: 20,\n                    ),\n                    label: '13.9 - 32.6',\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.navigation_rounded,\n                      fill: Color(0xff0385ff),\n                      size: 20,\n                    ),\n                    label: '8.0 - 13.8',\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.navigation_rounded,\n                      fill: Color(0xff03fff0),\n                      size: 20,\n                    ),\n                    label: '3.4 - 7.9',\n                  ),\n                  LegendItem(\n                    icon: const OutlinedIcon(\n                      Symbols.navigation_rounded,\n                      fill: Color(0xffffffff),\n                      size: 20,\n                    ),\n                    label: '0.1 - 3.3',\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_lib/utils.dart",
    "content": "/// Utilities, enumerations, and ID helpers shared across map layer managers.\nlibrary;\n\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// The available overlay layers that can be toggled on the DPIP map.\nenum MapLayer {\n  /// Real-time seismic monitor showing station intensities and EEW circles.\n  monitor,\n\n  /// Historical earthquake report list and detail overlay.\n  report,\n\n  /// Tsunami warning boundaries and observation data.\n  tsunami,\n\n  /// Weather radar echo tile overlay.\n  radar,\n\n  /// Temperature station circles and labels.\n  temperature,\n\n  /// Precipitation station circles and labels.\n  precipitation,\n\n  /// Wind direction arrows and speed labels.\n  wind,\n\n  /// Lightning strike symbol overlay.\n  lightning,\n}\n\n/// The subset of [MapLayer] values that relate to earthquake data.\nconst Set<MapLayer> kEarthquakeLayers = {\n  MapLayer.monitor,\n  MapLayer.report,\n  MapLayer.tsunami,\n};\n\n/// The subset of [MapLayer] values that relate to weather data.\nconst Set<MapLayer> kWeatherLayers = {\n  MapLayer.radar,\n  MapLayer.temperature,\n  MapLayer.precipitation,\n  MapLayer.wind,\n  MapLayer.lightning,\n};\n\n/// Defines which layer combinations are permitted when using overlay mode.\n///\n/// Each key maps to the full set of layers that may be active at the same time\n/// as that layer.\nconst Map<MapLayer, Set<MapLayer>> kAllowedLayerCombinations = {\n  MapLayer.monitor: {MapLayer.monitor},\n  MapLayer.report: {MapLayer.report},\n  MapLayer.tsunami: {MapLayer.tsunami},\n  MapLayer.radar: {\n    MapLayer.radar,\n    MapLayer.temperature,\n    MapLayer.precipitation,\n    MapLayer.wind,\n    MapLayer.lightning,\n  },\n  MapLayer.temperature: {MapLayer.radar, MapLayer.temperature},\n  MapLayer.precipitation: {MapLayer.radar, MapLayer.precipitation},\n  MapLayer.wind: {MapLayer.radar, MapLayer.wind},\n  MapLayer.lightning: {MapLayer.radar, MapLayer.lightning},\n};\n\n/// Validates if a combination of map layers follows the defined rules.\n///\n/// Returns true if:\n/// - earthquakeLayers.length ≤ 1 AND one of:\n///   - weatherLayers.length == 0\n///   - weatherLayers.length == 1\n///   - weatherLayers.length == 2 AND weatherLayers contains radar AND\n///     the other layer exists in _allowedRadarCombinations\n///\n/// Where:\n/// - earthquakeLayers = layers ∩ _earthquakeLayers\n/// - weatherLayers = layers ∩ _weatherLayers\nbool isValidLayerCombination(Set<MapLayer> layers) {\n  final earthquakeLayerCount = layers.where((l) => kEarthquakeLayers.contains(l)).length;\n  if (earthquakeLayerCount > 1) return false;\n\n  final weatherLayers = layers.where((l) => kWeatherLayers.contains(l)).toSet();\n\n  if (weatherLayers.length == 1) return true;\n\n  if (weatherLayers.length == 2) {\n    if (weatherLayers.contains(MapLayer.radar)) {\n      final otherLayer = weatherLayers.where((l) => l != MapLayer.radar).first;\n      return kAllowedLayerCombinations.containsKey(otherLayer);\n    }\n  }\n\n  return false;\n}\n\n/// Provides stable MapLibre GeoJSON source ID strings for each data type.\n///\n/// Pass an optional time or code suffix to obtain a time-specific ID,\n/// or omit it to get the base ID.\nclass MapSourceIds {\n  const MapSourceIds._();\n\n  /// Returns the source ID for radar data, optionally scoped to [time].\n  static String radar([String? time]) => time == null ? 'radar' : 'radar-$time';\n\n  /// Returns the source ID for earthquake report data, optionally scoped to\n  /// [time].\n  static String report([String? time]) => time == null ? 'report' : 'report-$time';\n\n  /// Returns the source ID for tsunami data, optionally scoped to [code].\n  static String tsunami([String? code]) => code == null ? 'tsunami' : 'tsunami-$code';\n\n  /// Returns the source ID for real-time seismograph (RTS) data, optionally\n  /// scoped to [time].\n  static String rts([String? time]) => time == null ? 'rts' : 'rts-$time';\n\n  /// Returns the source ID for EEW data, optionally scoped to [code].\n  static String eew([String? code]) => code == null ? 'eew' : 'eew-$code';\n\n  /// Returns the source ID for temperature data, optionally scoped to [time].\n  static String temperature([String? time]) => time == null ? 'temperature' : 'temperature-$time';\n\n  /// Returns the source ID for precipitation data, optionally scoped to\n  /// [time].\n  static String precipitation([String? time]) =>\n      time == null ? 'precipitation' : 'precipitation-$time';\n\n  /// Returns the source ID for wind data, optionally scoped to [time].\n  static String wind([String? time]) => time == null ? 'wind' : 'wind-$time';\n\n  /// Returns the source ID for lightning data, optionally scoped to [time].\n  static String lightning([String? time]) => time == null ? 'lightning' : 'lightning-$time';\n\n  /// Returns the source ID for seismic intensity polygon data.\n  static String intensity() => 'intensity';\n\n  /// Returns the source ID for zero-intensity seismic polygon data.\n  static String intensity0() => 'intensity0';\n\n  /// Returns the source ID for detection box data.\n  static String box() => 'box';\n}\n\n/// Provides stable MapLibre layer ID strings for each data type.\n///\n/// Pass an optional time or code suffix to obtain a time-specific ID,\n/// or omit it to get the base ID.\nclass MapLayerIds {\n  const MapLayerIds._();\n\n  /// Returns the layer ID for radar data, optionally scoped to [time].\n  static String radar([String? time]) => time == null ? 'radar' : 'radar-$time';\n\n  /// Returns the layer ID for earthquake report data, optionally scoped to\n  /// [time].\n  static String report([String? time]) => time == null ? 'report' : 'report-$time';\n\n  /// Returns the layer ID for tsunami data, optionally scoped to [code].\n  static String tsunami([String? code]) => code == null ? 'tsunami' : 'tsunami-$code';\n\n  /// Returns the layer ID for real-time seismograph (RTS) data, optionally\n  /// scoped to [time].\n  static String rts([String? time]) => time == null ? 'rts' : 'rts-$time';\n\n  /// Returns the layer ID for EEW data, optionally scoped to [code].\n  static String eew([String? code]) => code == null ? 'eew' : 'eew-$code';\n\n  /// Returns the layer ID for temperature data, optionally scoped to [time].\n  static String temperature([String? time]) => time == null ? 'temperature' : 'temperature-$time';\n\n  /// Returns the layer ID for precipitation data, optionally scoped to [time].\n  static String precipitation([String? time]) =>\n      time == null ? 'precipitation' : 'precipitation-$time';\n\n  /// Returns the layer ID for wind data, optionally scoped to [time].\n  static String wind([String? time]) => time == null ? 'wind' : 'wind-$time';\n\n  /// Returns the layer ID for lightning data, optionally scoped to [time].\n  static String lightning([String? time]) => time == null ? 'lightning' : 'lightning-$time';\n\n  /// Returns the layer ID for seismic intensity polygon data.\n  static String intensity() => 'intensity';\n\n  /// Returns the layer ID for zero-intensity seismic polygon data.\n  static String intensity0() => 'intensity0';\n\n  /// Returns the layer ID for detection box data.\n  static String box() => 'box';\n}\n\n/// Removes all non-base-map layers and sources from [controller].\n///\n/// Preserves layers listed in [BaseMapLayerIds.values] and the `map` source.\nFuture<void> cleanupMap(MapLibreMapController controller) async {\n  final layerIds = (await controller.getLayerIds()).cast<String>()\n    ..removeWhere((v) => BaseMapLayerIds.values().contains(v));\n  final sourceIds = (await controller.getSourceIds())..removeWhere((v) => v == 'map');\n\n  for (final layerId in layerIds) {\n    await controller.removeLayer(layerId);\n  }\n\n  for (final sourceId in sourceIds) {\n    await controller.removeSource(sourceId);\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/layer_toggle.dart",
    "content": "/// A toggleable layer-selection tile used inside the layer picker sheet.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A tappable tile representing a single map layer that can be toggled on or\n/// off.\n///\n/// Long-pressing triggers [onLongPress] when provided, which is used for\n/// overlay-mode activation.\nclass LayerToggle extends StatelessWidget {\n  /// Whether this layer is currently active.\n  final bool checked;\n\n  /// The human-readable name shown below the layer icon.\n  final String label;\n\n  /// Called when the user taps the tile. Pass `null` to disable interaction.\n  final void Function(bool)? onChanged;\n\n  /// Called when the user long-presses the tile for overlay mode.\n  final void Function(bool)? onLongPress;\n\n  /// Creates a [LayerToggle] for the given [label].\n  const LayerToggle({\n    super.key,\n    required this.checked,\n    required this.label,\n    required this.onChanged,\n    this.onLongPress,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final bool isDisabled = onChanged == null;\n    return Material(\n      color: Colors.transparent,\n      shape: RoundedRectangleBorder(borderRadius: .circular(16)),\n      child: Opacity(\n        opacity: isDisabled ? 0.4 : 1.0,\n        child: InkWell(\n          onTap: onChanged != null ? () => onChanged!(!checked) : null,\n          onLongPress: onLongPress != null ? () => onLongPress!(!checked) : null,\n          borderRadius: .circular(12),\n          child: Padding(\n            padding: const .all(6),\n            child: Column(\n              mainAxisSize: .min,\n              spacing: 4,\n              children: [\n                Container(\n                  decoration: BoxDecoration(\n                    border: Border.all(\n                      color: checked ? context.colors.primary : Colors.transparent,\n                      width: 2,\n                    ),\n                    borderRadius: .circular(12),\n                  ),\n                  padding: const .all(2),\n                  child: ClipRRect(\n                    borderRadius: .circular(8),\n                    child: Container(\n                      height: 64,\n                      width: 64,\n                      color: checked\n                          ? context.colors.primaryContainer\n                          : context.colors.surfaceContainerHighest,\n                      child: Icon(\n                        Symbols.layers_rounded,\n                        color: checked\n                            ? context.colors.onPrimaryContainer\n                            : context.colors.onSurface,\n                      ),\n                    ),\n                  ),\n                ),\n                Text(\n                  label,\n                  style: context.texts.labelMedium!.copyWith(\n                    color: checked ? context.colors.primary : context.colors.onSurfaceVariant,\n                    fontWeight: checked ? .bold : null,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/layer_toggle_sheet.dart",
    "content": "/// The bottom sheet widget that lets users toggle map layers and base maps.\nlibrary;\n\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/layer_toggle.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/sheet_container.dart';\nimport 'package:dpip/widgets/ui/labeled_divider.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A scrollable bottom sheet that exposes controls to toggle individual\n/// [MapLayer]s and choose among the available [BaseMapType]s.\nclass LayerToggleSheet extends StatefulWidget {\n  /// The currently active set of map layers.\n  final Set<MapLayer> activeLayers;\n\n  /// The currently selected base map style.\n  final BaseMapType currentBaseMap;\n\n  /// Called whenever a layer is turned on or off.\n  final void Function(MapLayer layer, bool state, Set<MapLayer> activeLayers) onLayerChanged;\n\n  /// Called whenever the user selects a different base map.\n  final void Function(BaseMapType baseMap) onBaseMapChanged;\n\n  /// Creates a [LayerToggleSheet] with the given layer and base-map state.\n  const LayerToggleSheet({\n    super.key,\n    required this.activeLayers,\n    required this.currentBaseMap,\n    required this.onLayerChanged,\n    required this.onBaseMapChanged,\n  });\n\n  @override\n  State<LayerToggleSheet> createState() => _LayerToggleSheetState();\n}\n\nclass _LayerToggleSheetState extends State<LayerToggleSheet> {\n  late Set<MapLayer> _activeLayers = Set.from(widget.activeLayers);\n  late BaseMapType _currentBaseMap = widget.currentBaseMap;\n\n  @override\n  void didUpdateWidget(LayerToggleSheet oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (!setEquals(oldWidget.activeLayers, widget.activeLayers)) {\n      setState(() => _activeLayers = widget.activeLayers);\n    }\n\n    if (oldWidget.currentBaseMap != widget.currentBaseMap) {\n      setState(() => _currentBaseMap = widget.currentBaseMap);\n    }\n  }\n\n  void _toggleLayer(MapLayer layer, {bool overlay = false}) {\n    final Set<MapLayer> newLayers;\n\n    if (overlay) {\n      newLayers = Set.from(_activeLayers);\n      final combination = kAllowedLayerCombinations[layer];\n      if (combination != null) {\n        newLayers.removeWhere((l) => !combination.contains(l));\n      }\n      newLayers.add(layer);\n    } else {\n      newLayers = {layer};\n    }\n\n    if (setEquals(_activeLayers, newLayers)) return;\n\n    final oldLayers = _activeLayers;\n    setState(() => _activeLayers = newLayers);\n\n    for (final removedLayer in oldLayers.difference(newLayers)) {\n      widget.onLayerChanged(removedLayer, false, newLayers);\n    }\n\n    for (final addedLayer in newLayers.difference(oldLayers)) {\n      widget.onLayerChanged(addedLayer, true, newLayers);\n    }\n  }\n\n  void _setBaseMap(BaseMapType baseMap) {\n    setState(() => _currentBaseMap = baseMap);\n    widget.onBaseMapChanged(baseMap);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SheetContainer(\n      icon: Symbols.layers_rounded,\n      title: Text('地圖圖層'.i18n),\n      description: Text('選擇要顯示的地圖圖層'.i18n),\n      child: Column(\n        mainAxisSize: .min,\n        crossAxisAlignment: .start,\n        spacing: 8,\n        children: [\n          LabeledDivider(label: '底圖'.i18n),\n          Wrap(\n            spacing: 4,\n            runSpacing: 8,\n            children: [\n              LayerToggle(\n                label: '簡單'.i18n,\n                checked: _currentBaseMap == .exptech,\n                onChanged: (_) => _setBaseMap(.exptech),\n              ),\n              LayerToggle(\n                label: 'OpenStreetMap',\n                checked: _currentBaseMap == .osm,\n                onChanged: (_) => _setBaseMap(.osm),\n              ),\n              LayerToggle(\n                label: 'Google',\n                checked: _currentBaseMap == .google,\n                onChanged: (_) => _setBaseMap(.google),\n              ),\n            ],\n          ),\n          LabeledDivider(label: '地震'.i18n),\n          Wrap(\n            spacing: 4,\n            runSpacing: 8,\n            children: [\n              LayerToggle(\n                label: '監視器'.i18n,\n                checked: _activeLayers.contains(MapLayer.monitor),\n                onChanged: (_) => _toggleLayer(.monitor),\n              ),\n              LayerToggle(\n                label: '報告'.i18n,\n                checked: _activeLayers.contains(MapLayer.report),\n                onChanged: (_) => _toggleLayer(.report),\n              ),\n              LayerToggle(\n                label: '海嘯'.i18n,\n                checked: _activeLayers.contains(MapLayer.tsunami),\n                onChanged: null,\n              ),\n            ],\n          ),\n          LabeledDivider(label: '氣象'.i18n),\n          Wrap(\n            spacing: 4,\n            runSpacing: 8,\n            children: [\n              LayerToggle(\n                label: '雷達回波'.i18n,\n                checked: _activeLayers.contains(MapLayer.radar),\n                onChanged: (_) => _toggleLayer(.radar),\n                onLongPress: (_) => _toggleLayer(.radar, overlay: true),\n              ),\n              LayerToggle(\n                label: '氣溫'.i18n,\n                checked: _activeLayers.contains(MapLayer.temperature),\n                onChanged: (_) => _toggleLayer(.temperature),\n                onLongPress: (_) => _toggleLayer(.temperature, overlay: true),\n              ),\n              LayerToggle(\n                label: '降水'.i18n,\n                checked: _activeLayers.contains(MapLayer.precipitation),\n                onChanged: (_) => _toggleLayer(.precipitation),\n                onLongPress: (_) => _toggleLayer(.precipitation, overlay: true),\n              ),\n              LayerToggle(\n                label: '風向/風速'.i18n,\n                checked: _activeLayers.contains(MapLayer.wind),\n                onChanged: (_) => _toggleLayer(.wind),\n                onLongPress: (_) => _toggleLayer(.wind, overlay: true),\n              ),\n              LayerToggle(\n                label: '閃電'.i18n,\n                checked: _activeLayers.contains(MapLayer.lightning),\n                onChanged: (_) => _toggleLayer(.lightning),\n                onLongPress: (_) => _toggleLayer(.lightning, overlay: true),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/map_legend.dart",
    "content": "/// Legend widgets used in the map overlay panels.\nlibrary;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/layout.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\n\n/// A single entry in a [ColorLegend], pairing a colour swatch with a value.\nclass ColorLegendItem {\n  /// The colour displayed in the swatch for this entry.\n  final Color color;\n\n  /// An optional override label; defaults to [value] when `null`.\n  final String? label;\n\n  /// The numeric value associated with this colour band.\n  final num value;\n\n  /// Whether the top edge of this swatch blends with the previous item's\n  /// colour.\n  final bool blendHead;\n\n  /// Whether the bottom edge of this swatch blends with the next item's\n  /// colour.\n  final bool blendTail;\n\n  /// When `true`, this item is omitted from the rendered legend.\n  final bool hidden;\n\n  /// Creates a [ColorLegendItem] with the required [color] and [value].\n  ColorLegendItem({\n    required this.color,\n    required this.value,\n    this.label,\n    this.blendHead = true,\n    this.blendTail = true,\n    this.hidden = false,\n  });\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) {\n      return true;\n    }\n\n    if (other is ColorLegendItem) {\n      return color == other.color &&\n          value == other.value &&\n          label == other.label &&\n          blendHead == other.blendHead &&\n          blendTail == other.blendTail &&\n          hidden == other.hidden;\n    }\n\n    return false;\n  }\n\n  @override\n  int get hashCode => Object.hash(color, value, blendHead, blendTail, hidden);\n}\n\n/// A vertical colour-scale legend rendered as blended gradient swatches.\n///\n/// Set [reverse] to `true` to display [items] from bottom to top. Provide\n/// [unit] to show a unit label; set [appendUnit] to append it inline next to\n/// each value instead of below the legend.\nclass ColorLegend extends StatelessWidget {\n  /// The list of colour band items to display.\n  final List<ColorLegendItem> items;\n\n  /// Whether to display items in reverse order.\n  final bool reverse;\n\n  /// The unit string shown below or inline with the legend values.\n  final String? unit;\n\n  /// When `true`, appends [unit] after each value label instead of below.\n  final bool appendUnit;\n\n  /// Creates a [ColorLegend] from the given [items].\n  const ColorLegend({\n    super.key,\n    required this.items,\n    this.reverse = false,\n    this.appendUnit = false,\n    this.unit,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final items = reverse ? this.items.reversed.toList() : this.items;\n    final visibleItems = items.where((item) => !item.hidden).toList();\n\n    final children = items.mapIndexed((index, item) {\n      if (item.hidden) return const SizedBox.shrink();\n\n      final visibleIndex = visibleItems.indexOf(item);\n\n      final previous = index == 0 ? null : items.elementAtOrNull(index - 1);\n      final next = items.elementAtOrNull(index + 1);\n\n      final headColor = item.blendHead\n          ? (previous != null\n                ? Color.alphaBlend(\n                    item.color.withValues(alpha: 0.5),\n                    previous.color,\n                  )\n                : item.color)\n          : item.color;\n      final tailColor = item.blendTail\n          ? (next != null\n                ? Color.alphaBlend(\n                    item.color.withValues(alpha: 0.5),\n                    next.color,\n                  )\n                : item.color)\n          : item.color;\n\n      return IntrinsicHeight(\n        child: Layout.row.stretch[6](\n          children: [\n            if (!item.blendHead && !item.blendTail)\n              ColoredBox(color: item.color)\n            else\n              Container(\n                decoration: BoxDecoration(\n                  gradient: LinearGradient(\n                    colors: [headColor, item.color, tailColor],\n                    begin: .topCenter,\n                    end: .bottomCenter,\n                  ),\n                  borderRadius: visibleIndex == 0\n                      ? const .vertical(top: Radius.circular(8))\n                      : (visibleIndex + 1) == visibleItems.length\n                      ? const .vertical(bottom: Radius.circular(8))\n                      : null,\n                ),\n                width: 8,\n              ),\n            RichText(\n              text: TextSpan(\n                children: [\n                  TextSpan(text: item.label ?? '${item.value}'),\n                  if (unit != null && appendUnit)\n                    TextSpan(\n                      text: ' $unit',\n                      style: TextStyle(color: context.colors.outline),\n                    ),\n                ],\n                style: context.texts.labelSmall?.copyWith(\n                  color: context.colors.onSurfaceVariant,\n                  fontFeatures: const [FontFeature.tabularFigures()],\n                ),\n              ),\n            ),\n          ],\n        ),\n      );\n    }).toList();\n\n    return Layout.col.left[2](\n      children: [\n        Layout.col.left(children: children),\n        if (unit != null && !appendUnit)\n          Text(\n            '單位：{unit}'.i18n.args({'unit': unit!}),\n            style: context.texts.labelSmall?.copyWith(\n              color: context.colors.onSurfaceVariant,\n            ),\n          ),\n      ],\n    );\n  }\n}\n\n/// A single entry in a [Legend], combining an icon widget with a text label.\nclass LegendItem {\n  /// The icon widget displayed beside [label].\n  final Widget icon;\n\n  /// The descriptive text for this legend entry.\n  final String label;\n\n  /// Creates a [LegendItem] with the required [icon] and [label].\n  LegendItem({required this.icon, required this.label});\n}\n\n/// A vertical icon-based legend for categorical map data.\n///\n/// Set [reverse] to `true` to display [items] from bottom to top. Provide\n/// [unit] to show a unit label; set [appendUnit] to append it inline.\nclass Legend extends StatelessWidget {\n  /// The list of icon-label entries to display.\n  final List<LegendItem> items;\n\n  /// Whether to display items in reverse order.\n  final bool reverse;\n\n  /// The unit string shown below or inline with the legend values.\n  final String? unit;\n\n  /// When `true`, appends [unit] after each label instead of below.\n  final bool appendUnit;\n\n  /// Creates a [Legend] from the given [items].\n  const Legend({\n    super.key,\n    required this.items,\n    this.reverse = false,\n    this.appendUnit = false,\n    this.unit,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final items = reverse ? this.items.reversed.toList() : this.items;\n\n    final children = items.map((item) {\n      return Layout.row.left[2](\n        children: [\n          item.icon,\n          RichText(\n            text: TextSpan(\n              children: [\n                TextSpan(text: item.label),\n                if (unit != null && appendUnit)\n                  TextSpan(\n                    text: ' $unit',\n                    style: TextStyle(color: context.colors.outline),\n                  ),\n              ],\n              style: context.texts.labelSmall?.copyWith(\n                color: context.colors.onSurfaceVariant,\n                fontFeatures: const [FontFeature.tabularFigures()],\n              ),\n            ),\n          ),\n        ],\n      );\n    }).toList();\n\n    return Layout.col.left[2](\n      children: [\n        Layout.col.left(children: children),\n        if (unit != null && !appendUnit)\n          Text(\n            '單位：{unit}'.i18n.args({'unit': unit!}),\n            style: context.texts.labelSmall?.copyWith(\n              color: context.colors.onSurfaceVariant,\n            ),\n          ),\n      ],\n    );\n  }\n}\n\n/// An icon rendered with an independent fill colour and outline colour.\n///\n/// Achieved by stacking two [Icon] widgets: one filled and one stroked.\nclass OutlinedIcon extends StatelessWidget {\n  /// The icon glyph to render.\n  final IconData icon;\n\n  /// The interior fill colour of the icon.\n  final Color? fill;\n\n  /// The outline stroke colour of the icon.\n  final Color? stroke;\n\n  /// The logical pixel size of the icon.\n  final double? size;\n\n  /// Creates an [OutlinedIcon] for [icon].\n  const OutlinedIcon(this.icon, {super.key, this.fill, this.stroke, this.size});\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Icon(icon, fill: 1, color: fill, size: size),\n        Icon(icon, fill: 0, color: stroke, size: size),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/sheet.dart",
    "content": "/// A generic draggable bottom sheet used on the map page.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// A [DraggableScrollableSheet] that fills the screen and constrains its\n/// content to the standard bottom-sheet width.\n///\n/// Pass [initialSize] as a fraction of the screen height to control how far\n/// the sheet is open on first render.\nclass CustomSheet extends StatefulWidget {\n  /// The widgets displayed inside the scrollable sheet.\n  final List<Widget> children;\n\n  /// The initial fractional height of the sheet relative to the screen.\n  ///\n  /// Defaults to 40 % of the bottom-sheet max height when `null`.\n  final double? initialSize;\n\n  /// Creates a [CustomSheet] with the given [children].\n  const CustomSheet({super.key, required this.children, this.initialSize});\n\n  @override\n  State<CustomSheet> createState() => _CustomSheetState();\n}\n\nclass _CustomSheetState extends State<CustomSheet> {\n  final DraggableScrollableController _controller = DraggableScrollableController();\n\n  @override\n  Widget build(BuildContext context) {\n    final screenHeight = context.dimension.height;\n    final defaultInitialSize = (context.bottomSheetConstraints.maxHeight * 0.4) / screenHeight;\n\n    return Positioned.fill(\n      child: DraggableScrollableSheet(\n        controller: _controller,\n        initialChildSize: widget.initialSize ?? defaultInitialSize,\n        minChildSize: 64 / screenHeight,\n        maxChildSize: context.bottomSheetConstraints.maxHeight / screenHeight,\n        snap: true,\n        snapSizes: const [0.25],\n        builder: (context, scrollController) {\n          return Center(\n            child: ConstrainedBox(\n              constraints: context.bottomSheetConstraints,\n              child: Material(\n                color: context.colors.surface,\n                surfaceTintColor: context.colors.surfaceTint,\n                elevation: 1,\n                clipBehavior: Clip.hardEdge,\n                shape: const RoundedRectangleBorder(\n                  borderRadius: .vertical(top: Radius.circular(28)),\n                ),\n                child: ListView(\n                  controller: scrollController,\n                  shrinkWrap: true,\n                  children: widget.children,\n                ),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/tos_sheet.dart",
    "content": "/// The terms-of-service sheet shown before activating the seismic monitor.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A bottom sheet that presents the TREM monitor terms of service.\n///\n/// The user must scroll to the bottom to unlock the agree button. Popping with\n/// `true` means agreement; `false` means refusal.\nclass TosBottomSheet extends StatefulWidget {\n  /// Creates a [TosBottomSheet].\n  const TosBottomSheet({super.key});\n\n  @override\n  State<TosBottomSheet> createState() => _TosBottomSheetState();\n}\n\nclass _TosBottomSheetState extends State<TosBottomSheet> {\n  bool _isAgreeUnlocked = false;\n  final _controller = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    _controller.addListener(() {\n      if (_controller.position.atEdge && _controller.position.pixels > 0) {\n        setState(() => _isAgreeUnlocked = true);\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SafeArea(\n      child: Column(\n        children: [\n          Expanded(\n            child: LayoutBuilder(\n              builder: (context, constraints) {\n                return SingleChildScrollView(\n                  controller: _controller,\n                  padding: const .all(16),\n                  child: ConstrainedBox(\n                    constraints: BoxConstraints(\n                      minHeight: constraints.maxHeight + 1,\n                    ),\n                    child: IntrinsicHeight(\n                      child: Column(\n                        mainAxisSize: .min,\n                        spacing: 16,\n                        children: [\n                          Padding(\n                            padding: const .symmetric(vertical: 16),\n                            child: Column(\n                              children: [\n                                Icon(\n                                  Symbols.monitor_heart_rounded,\n                                  size: 36,\n                                  color: context.colors.secondary,\n                                ),\n                                const SizedBox(height: 16),\n                                Text(\n                                  '強震監視器',\n                                  style: context.texts.headlineMedium,\n                                  textAlign: .center,\n                                ),\n                              ],\n                            ),\n                          ),\n                          Padding(\n                            padding: const .all(16),\n                            child: Text(\n                              '在 DPIP 中可以查看來自 ExpTech 旗下 TREM 之強震監視器服務，請詳細閱讀以下條件，並選擇是否啟用。',\n                              style: context.texts.bodyLarge,\n                            ),\n                          ),\n                          Container(\n                            padding: const .all(16),\n                            decoration: BoxDecoration(\n                              color: context.colors.errorContainer,\n                              borderRadius: .circular(16),\n                            ),\n                            child: Text(\n                              '顯示的即時震度不是中央氣象署所提供之資料，因此可能與中央氣象署觀測到的結果不一致，應以中央氣象署公布之資訊為主。',\n                              style: context.texts.bodyLarge!.copyWith(\n                                color: context.colors.onErrorContainer,\n                              ),\n                            ),\n                          ),\n                          Container(\n                            padding: const .all(16),\n                            decoration: BoxDecoration(\n                              color: context.colors.errorContainer,\n                              borderRadius: .circular(16),\n                            ),\n                            child: Text(\n                              '強震監視器使用之測站為 ExpTech 所有，不歸中央氣象署管理，請不要向中央氣象署傳遞故障或意見，會造成他們的困擾。',\n                              style: context.texts.bodyLarge!.copyWith(\n                                color: context.colors.onErrorContainer,\n                              ),\n                            ),\n                          ),\n                          Container(\n                            padding: const .all(16),\n                            decoration: BoxDecoration(\n                              color: context.colors.surfaceContainer,\n                              borderRadius: .circular(16),\n                            ),\n                            child: Text(\n                              '強震監視器是由 TREM（臺灣即時地震監測）觀測到全臺現在的震動，做為即時震度顯示的功能，地震發生當下可以透過站點顏色變化，觀察地震波傳播情形。',\n                              style: context.texts.bodyLarge,\n                            ),\n                          ),\n                          Container(\n                            padding: const .all(16),\n                            decoration: BoxDecoration(\n                              color: context.colors.surfaceContainer,\n                              borderRadius: .circular(16),\n                            ),\n                            child: Text(\n                              '由於日常雜訊（汽車、工廠、施工等）影響，平時站點可能也會有顏色變化。另外，由於是即時資料，當下無法判斷是否是故障，所以也有可能因為站點故障而改變顏色。',\n                              style: context.texts.bodyLarge,\n                            ),\n                          ),\n                          Container(\n                            padding: const .all(16),\n                            decoration: BoxDecoration(\n                              color: context.colors.surfaceContainer,\n                              borderRadius: .circular(16),\n                            ),\n                            child: Text(\n                              '2022 年 6 月初開始於全臺各地部署站點，TREM-Net（TREM 地震觀測網）由兩個觀測網組成，分別為 SE-Net（強震觀測網「加速度儀」）及 MS-Net（微震觀測網「速度儀」），共同紀錄地震時的各項數據。',\n                              style: context.texts.bodyLarge,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                );\n              },\n            ),\n          ),\n          const Divider(height: 1),\n          Padding(\n            padding: const .symmetric(horizontal: 16, vertical: 8),\n            child: Row(\n              mainAxisAlignment: .spaceBetween,\n              children: [\n                TextButton(\n                  onPressed: () => context.pop(false),\n                  child: const Text('不同意'),\n                ),\n                FilledButton(\n                  onPressed: _isAgreeUnlocked ? () => context.pop(true) : null,\n                  child: const Text('同意'),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/ui/positioned_back_button.dart",
    "content": "/// A back-navigation button positioned in the top-left corner of the map.\nlibrary;\n\nimport 'package:dpip/app/home/_widgets/blurred_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A blurred icon button fixed at the top-left of the map that triggers\n/// [onPressed] when tapped.\nclass PositionedBackButton extends StatelessWidget {\n  /// Called when the button is tapped.\n  final VoidCallback? onPressed;\n\n  /// Creates a [PositionedBackButton] with an optional [onPressed] callback.\n  const PositionedBackButton({super.key, this.onPressed});\n\n  @override\n  Widget build(BuildContext context) {\n    return Positioned(\n      top: 24,\n      left: 24,\n      child: SafeArea(\n        child: BlurredIconButton(\n          icon: const Icon(Symbols.arrow_back_rounded),\n          onPressed: onPressed,\n          elevation: 2,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/_widgets/ui/positioned_layer_button.dart",
    "content": "/// A layer-picker button positioned in the top-right corner of the map.\nlibrary;\n\nimport 'package:dpip/app/home/_widgets/blurred_button.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/layer_toggle_sheet.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A blurred icon button fixed at the top-right of the map that opens the\n/// [LayerToggleSheet] when tapped.\n///\n/// Hidden when [isReplayMode] is `true`.\nclass PositionedLayerButton extends StatelessWidget {\n  /// The currently active set of map layers.\n  final Set<MapLayer> activeLayers;\n\n  /// The currently selected base map style.\n  final BaseMapType currentBaseMap;\n\n  /// When `true`, hides this button entirely.\n  final bool isReplayMode;\n\n  /// Called when the user toggles a layer on or off.\n  final void Function(MapLayer layer, bool show, Set<MapLayer> activeLayers) onLayerChanged;\n\n  /// Called when the user selects a different base map.\n  final void Function(BaseMapType baseMap) onBaseMapChanged;\n\n  /// Creates a [PositionedLayerButton] with the required layer and base-map\n  /// callbacks.\n  const PositionedLayerButton({\n    super.key,\n    required this.activeLayers,\n    required this.currentBaseMap,\n    required this.isReplayMode,\n    required this.onLayerChanged,\n    required this.onBaseMapChanged,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    if (isReplayMode) return const SizedBox.shrink();\n    return Positioned(\n      top: 24,\n      right: 24,\n      child: SafeArea(\n        child: BlurredIconButton(\n          icon: const Icon(Symbols.layers_rounded),\n          tooltip: '圖層',\n          elevation: 2,\n          onPressed: () => showModalBottomSheet(\n            context: context,\n            useRootNavigator: true,\n            useSafeArea: true,\n            isScrollControlled: true,\n            constraints: context.bottomSheetConstraints,\n            builder: (context) {\n              return LayerToggleSheet(\n                activeLayers: activeLayers,\n                currentBaseMap: currentBaseMap,\n                onLayerChanged: onLayerChanged,\n                onBaseMapChanged: onBaseMapChanged,\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/map/page.dart",
    "content": "/// The main map page and supporting options for the DPIP map view.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/app/map/_lib/manager.dart';\nimport 'package:dpip/app/map/_lib/managers/lightning.dart';\nimport 'package:dpip/app/map/_lib/managers/monitor.dart';\nimport 'package:dpip/app/map/_lib/managers/precipitation.dart';\nimport 'package:dpip/app/map/_lib/managers/radar.dart';\nimport 'package:dpip/app/map/_lib/managers/report.dart';\nimport 'package:dpip/app/map/_lib/managers/temperature.dart';\nimport 'package:dpip/app/map/_lib/managers/tsunami.dart';\nimport 'package:dpip/app/map/_lib/managers/wind.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/app/map/_widgets/ui/positioned_back_button.dart';\nimport 'package:dpip/app/map/_widgets/ui/positioned_layer_button.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/maplibre.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Configuration options passed to [MapPage] to control initial state.\nclass MapPageOptions {\n  /// The set of [MapLayer]s to activate when the page first appears.\n  final Set<MapLayer>? initialLayers;\n\n  /// The report ID to load and display immediately on open.\n  final String? reportId;\n\n  /// A Unix timestamp (ms) used to replay monitor data at a fixed point in time.\n  final int? replayTimestamp;\n\n  /// Creates options with optional initial layers, report ID, and replay\n  /// timestamp.\n  MapPageOptions({this.initialLayers, this.reportId, this.replayTimestamp});\n\n  /// Constructs [MapPageOptions] by parsing URL query parameters.\n  ///\n  /// Reads `layers`, `report`, and `replay` keys from [queryParameters].\n  factory MapPageOptions.fromQueryParameters(\n    Map<String, String> queryParameters,\n  ) {\n    final layers = queryParameters['layers']?.split(',');\n    final report = queryParameters['report'];\n    final replay = queryParameters['replay'];\n\n    return MapPageOptions(\n      initialLayers: layers?.map((layer) => MapLayer.values.asNameMap()[layer]).whereType<MapLayer>().toSet(),\n      reportId: report,\n      replayTimestamp: replay == null ? null : int.tryParse(replay),\n    );\n  }\n}\n\n/// The full-screen map page with layer management and playback controls.\nclass MapPage extends StatefulWidget {\n  /// Optional configuration controlling the initial layer and report state.\n  final MapPageOptions? options;\n\n  /// Creates a [MapPage] with optional [options].\n  const MapPage({super.key, this.options});\n\n  /// Returns the route path for this page, including any query parameters\n  /// derived from [options].\n  static String route({MapPageOptions? options}) {\n    if (options == null) return '/map';\n\n    final parameters = [];\n\n    if (options.initialLayers != null)\n      parameters.add(\n        'layers=${options.initialLayers!.map((e) => e.name).join(',')}',\n      );\n    if (options.reportId != null) parameters.add('report=${options.reportId}');\n    if (options.replayTimestamp != null) parameters.add('replay=${options.replayTimestamp}');\n\n    return \"/map?${parameters.join('&')}\";\n  }\n\n  @override\n  State<MapPage> createState() => _MapPageState();\n}\n\nclass _MapPageState extends State<MapPage> with TickerProviderStateMixin {\n  late final MapLibreMapController _controller;\n  final _managers = <MapLayer, MapLayerManager>{};\n\n  Timer? _ticker;\n  late BaseMapType _baseMapType = GlobalProviders.map.baseMap;\n\n  late Set<MapLayer> _activeLayers =\n      widget.options?.initialLayers ??\n      (widget.options?.replayTimestamp != null ? {MapLayer.monitor} : {});\n  Future<void>? _toggleLayerOperation;\n\n  void _setupTicker() {\n    _ticker?.cancel();\n    _ticker = Timer.periodic(\n      Duration(\n        milliseconds: 1000 ~/ GlobalProviders.map.updateIntervalNotifier.value,\n      ),\n      (timer) {\n        for (final layer in _activeLayers) {\n          _managers[layer]?.tick();\n        }\n      },\n    );\n  }\n\n  /// The currently active set of map layers.\n  Set<MapLayer> get activeLayers => _activeLayers;\n\n  /// Returns the highest-priority active weather layer, or the first active\n  /// layer if none of the priority layers are active.\n  MapLayer? get primaryLayer {\n    for (final layer in [\n      MapLayer.temperature,\n      MapLayer.precipitation,\n      MapLayer.wind,\n      MapLayer.lightning,\n    ]) {\n      if (_activeLayers.contains(layer)) {\n        return layer;\n      }\n    }\n    return _activeLayers.isNotEmpty ? _activeLayers.first : null;\n  }\n\n  /// Synchronises the radar layer to the given weather observation [time].\n  ///\n  /// Has no effect if the radar layer is not currently active.\n  Future<void> syncTimeToRadar(String time) async {\n    if (!_activeLayers.contains(MapLayer.radar)) return;\n\n    final radarManager = getLayerManager<RadarMapLayerManager>(MapLayer.radar);\n    if (radarManager != null) {\n      try {\n        await radarManager.updateRadarTime(time);\n      } catch (e, s) {\n        TalkerManager.instance.error('Failed to sync radar time', e, s);\n      }\n    }\n  }\n\n  void _setupWeatherLayerTimeSync() {\n    final temperatureManager = getLayerManager<TemperatureMapLayerManager>(\n      MapLayer.temperature,\n    );\n    temperatureManager?.onTimeChanged = (time) {\n      syncTimeToRadar(time);\n    };\n\n    final precipitationManager = getLayerManager<PrecipitationMapLayerManager>(\n      MapLayer.precipitation,\n    );\n    precipitationManager?.onTimeChanged = (time) {\n      syncTimeToRadar(time);\n    };\n\n    final windManager = getLayerManager<WindMapLayerManager>(MapLayer.wind);\n    windManager?.onTimeChanged = (time) {\n      syncTimeToRadar(time);\n    };\n\n    final lightningManager = getLayerManager<LightningMapLayerManager>(\n      MapLayer.lightning,\n    );\n    lightningManager?.onTimeChanged = (time) {\n      syncTimeToRadar(time);\n    };\n  }\n\n  Future<void> _syncRadarTimeOnCombination(MapLayer newLayer) async {\n    if (!_activeLayers.contains(MapLayer.radar) ||\n        !kWeatherLayers.contains(newLayer) ||\n        newLayer == MapLayer.radar) {\n      return;\n    }\n\n    String? newTime;\n    switch (newLayer) {\n      case MapLayer.temperature:\n        final manager = getLayerManager<TemperatureMapLayerManager>(\n          MapLayer.temperature,\n        );\n        newTime = manager?.currentTemperatureTime.value;\n      case MapLayer.precipitation:\n        final manager = getLayerManager<PrecipitationMapLayerManager>(\n          MapLayer.precipitation,\n        );\n        newTime = manager?.currentPrecipitationTime.value;\n      case MapLayer.wind:\n        final manager = getLayerManager<WindMapLayerManager>(MapLayer.wind);\n        newTime = manager?.currentWindTime.value;\n      case MapLayer.lightning:\n        final manager = getLayerManager<LightningMapLayerManager>(\n          MapLayer.lightning,\n        );\n        newTime = manager?.currentLightningTime.value;\n      default:\n    }\n\n    if (newTime != null) {\n      await syncTimeToRadar(newTime);\n    }\n  }\n\n  /// Returns the [MapLayerManager] for [layer] cast to [T], or `null` if the\n  /// manager does not exist or is not of type [T].\n  T? getLayerManager<T extends MapLayerManager>(MapLayer layer) {\n    final manager = _managers[layer];\n    return manager is T ? manager : null;\n  }\n\n  /// Toggles [layer] on or off, waiting for any prior toggle to complete first.\n  ///\n  /// Pass [show] as `true` to show the layer or `false` to hide it.\n  /// [activeLayers] is the full desired set of active layers after the change.\n  Future<void> toggleLayer(\n    MapLayer layer,\n    bool show,\n    Set<MapLayer> activeLayers,\n  ) async {\n    // Wait for any pending operations to complete\n    await _toggleLayerOperation;\n\n    // Queue this operation\n    _toggleLayerOperation = _performToggleLayer(layer, show, activeLayers);\n    await _toggleLayerOperation;\n  }\n\n  Future<void> _performToggleLayer(\n    MapLayer layer,\n    bool show,\n    Set<MapLayer> activeLayers,\n  ) async {\n    if (!mounted) return;\n\n    // Update state immediately to prevent race conditions\n    final previousLayers = _activeLayers;\n    setState(() => _activeLayers = activeLayers);\n\n    try {\n      final manager = _managers[layer];\n      if (manager == null) {\n        throw UnimplementedError('Unknown layer: $layer');\n      }\n\n      if (show) {\n        if (!manager.didSetup) await manager.setup();\n        await manager.show();\n        await _syncRadarTimeOnCombination(layer);\n      } else {\n        await manager.hide();\n      }\n\n      if (_activeLayers.isEmpty) {\n        await _controller.animateCamera(\n          CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n        );\n      }\n    } catch (e, s) {\n      // Revert state on error\n      setState(() => _activeLayers = previousLayers);\n      TalkerManager.instance.error('_MapPageState.toggleLayer', e, s);\n    }\n  }\n\n  /// Switches the base map style to [baseMapType] and updates state.\n  Future<void> setBaseMapType(BaseMapType baseMapType) async {\n    if (!mounted) return;\n\n    await _controller.setBaseMap(baseMapType);\n\n    setState(() => _baseMapType = baseMapType);\n  }\n\n  /// Sets up and shows each layer in [layers], replacing the active layer set.\n  Future<void> setLayers(Set<MapLayer> layers) async {\n    if (!mounted) return;\n\n    for (final layer in layers) {\n      final manager = _managers[layer];\n      if (manager != null) {\n        if (!manager.didSetup) await manager.setup();\n        await manager.show();\n      }\n    }\n\n    setState(() => _activeLayers = layers);\n  }\n\n  /// Called by [DpipMap] when the underlying MapLibre controller is ready.\n  void onMapCreated(MapLibreMapController controller) {\n    setState(() => _controller = controller);\n  }\n\n  /// Called after the map style has finished loading.\n  ///\n  /// Rebuilds all [MapLayerManager] instances and restores any active layers.\n  void onStyleLoaded() {\n    final controller = _controller;\n\n    for (final manager in _managers.values) {\n      manager.dispose();\n    }\n\n    _managers[MapLayer.monitor] = MonitorMapLayerManager(\n      context,\n      controller,\n      isReplayMode: widget.options?.replayTimestamp != null,\n      replayTimestamp: widget.options?.replayTimestamp,\n    );\n    _managers[MapLayer.report] = ReportMapLayerManager(\n      context,\n      controller,\n      initialReportId: widget.options?.reportId,\n    );\n    _managers[MapLayer.tsunami] = TsunamiMapLayerManager(context, controller);\n    _managers[MapLayer.radar] = RadarMapLayerManager(\n      context,\n      controller,\n      getActiveLayerCount: () => _activeLayers.length,\n    );\n    _managers[MapLayer.temperature] = TemperatureMapLayerManager(\n      context,\n      controller,\n    );\n    _managers[MapLayer.precipitation] = PrecipitationMapLayerManager(\n      context,\n      controller,\n    );\n    _managers[MapLayer.wind] = WindMapLayerManager(context, controller);\n    _managers[MapLayer.lightning] = LightningMapLayerManager(\n      context,\n      controller,\n    );\n\n    _setupWeatherLayerTimeSync();\n\n    setLayers(_activeLayers);\n  }\n\n  void _handleBack() {\n    for (final layer in _activeLayers) {\n      final manager = _managers[layer];\n      if (manager != null && !manager.shouldPop) {\n        manager.onPopInvoked();\n        return;\n      }\n    }\n    if (context.canPop()) {\n      context.pop();\n    } else {\n      HomeRoute().go(context);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    GlobalProviders.map.updateIntervalNotifier.addListener(_setupTicker);\n    _setupTicker();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Stack(\n        children: [\n          DpipMap(\n            baseMapType: _baseMapType,\n            onMapCreated: onMapCreated,\n            onStyleLoadedCallback: onStyleLoaded,\n          ),\n          PositionedLayerButton(\n            activeLayers: _activeLayers,\n            currentBaseMap: _baseMapType,\n            isReplayMode: widget.options?.replayTimestamp != null,\n            onLayerChanged: toggleLayer,\n            onBaseMapChanged: setBaseMapType,\n          ),\n          PositionedBackButton(onPressed: _handleBack),\n          ..._activeLayers.map((layer) {\n            final manager = _managers[layer];\n            if (manager != null) {\n              return manager.build(context);\n            }\n            return const SizedBox.shrink();\n          }),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _ticker?.cancel();\n    for (final manager in _managers.values) {\n      manager.dispose();\n    }\n    GlobalProviders.map.updateIntervalNotifier.removeListener(_setupTicker);\n\n    super.dispose();\n  }\n}\n\n/// A convenience wrapper around [MapPage] that opens directly in monitor\n/// replay mode for the given [replayTimestamp].\nclass MapMonitorPage extends StatelessWidget {\n  /// The Unix timestamp (ms) at which to replay monitor data.\n  final int replayTimestamp;\n\n  /// Creates a [MapMonitorPage] with the required [replayTimestamp].\n  const MapMonitorPage({super.key, required this.replayTimestamp});\n\n  @override\n  Widget build(BuildContext context) {\n    return MapPage(\n      options: MapPageOptions(\n        initialLayers: {MapLayer.monitor},\n        replayTimestamp: replayTimestamp,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/_widgets/settings_header.dart",
    "content": "/// Reusable header widget for settings pages.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/cupertino.dart';\n\n/// A header widget for settings pages, displaying an icon alongside a title\n/// and subtitle.\n///\n/// Use at the top of a settings page to give it a consistent visual identity:\n///\n/// ```dart\n/// SettingsHeader(\n///   icon: Symbols.straighten_rounded,\n///   iconColor: Colors.amberAccent,\n///   title: Text('Unit'),\n///   subtitle: Text('Customize display units'),\n/// )\n/// ```\n///\n/// When [iconColor] is omitted, the icon uses the theme's\n/// [ColorScheme.onPrimaryContainer] color on a [ColorScheme.primaryContainer]\n/// background.\nclass SettingsHeader extends StatelessWidget {\n  /// The icon displayed inside the [ContainedIcon].\n  final IconData icon;\n\n  /// The color of the icon. Defaults to the theme's [ColorScheme.onPrimaryContainer]\n  /// with [ColorScheme.primaryContainer] as the background.\n  final Color? iconColor;\n\n  /// The primary label for this settings section.\n  final Widget title;\n\n  /// A short description shown below [title].\n  final Widget subtitle;\n\n  /// Creates a [SettingsHeader].\n  const SettingsHeader({\n    super.key,\n    required this.icon,\n    this.iconColor,\n    required this.title,\n    required this.subtitle,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: .symmetric(horizontal: 16, vertical: 8),\n      child: Row(\n        spacing: 12,\n        children: [\n          ContainedIcon(\n            icon,\n            color: iconColor ?? context.colors.onPrimaryContainer,\n            backgroundColor: iconColor == null ? context.colors.primaryContainer : null,\n            size: 28,\n          ),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: .start,\n              children: [\n                DefaultTextStyle(\n                  style: context.texts.titleLarge!.copyWith(fontWeight: .bold),\n                  child: title,\n                ),\n                DefaultTextStyle(\n                  style: context.texts.bodyLarge!,\n                  child: subtitle,\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/donate/page.dart",
    "content": "/// Donate settings page for in-app purchase and donation options.\nlibrary;\n\nimport 'dart:async';\nimport 'dart:io';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/product_detail.dart';\nimport 'package:dpip/utils/functions.dart';\nimport 'package:flutter/material.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:in_app_purchase/in_app_purchase.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// A page that presents subscription and one-time donation options via\n/// in-app purchases.\n///\n/// Pull to refresh to reload the product list from the store.\nclass SettingsDonatePage extends StatefulWidget {\n  /// Creates a [SettingsDonatePage].\n  const SettingsDonatePage({super.key});\n\n  @override\n  State<SettingsDonatePage> createState() => _SettingsDonatePageState();\n}\n\nclass _SettingsDonatePageState extends State<SettingsDonatePage>\n    with SingleTickerProviderStateMixin {\n  bool isPending = false;\n  String? processingProductId;\n  final Set<String> purchasedProductIds = {};\n\n  final _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();\n  Completer<List<ProductDetails>> products = Completer();\n\n  late AnimationController _shimmerController;\n\n  final Set<String> _kIds = <String>{\n    's_donation75',\n    'donation100',\n    'donation300',\n    'donation1000',\n  };\n  StreamSubscription<List<PurchaseDetails>>? subscription;\n\n  Future<void> refresh() async {\n    setState(() {\n      products = Completer<List<ProductDetails>>();\n      purchasedProductIds.clear();\n    });\n\n    final isAvailable = await InAppPurchase.instance.isAvailable();\n\n    if (!isAvailable) {\n      products.completeError('無法連線至商店，請稍後再試'.i18n);\n      return;\n    }\n\n    final ProductDetailsResponse response = await InAppPurchase.instance.queryProductDetails(_kIds);\n    if (response.notFoundIDs.isNotEmpty) {\n      products.completeError('找不到商品，請稍候再試'.i18n);\n      return;\n    }\n\n    products.complete(response.productDetails);\n  }\n\n  void onPurchaseUpdate(List<PurchaseDetails> list) {\n    if (!mounted) return;\n\n    final hasAnyPending = list.any((d) => d.status == PurchaseStatus.pending);\n\n    setState(() {\n      isPending = hasAnyPending;\n      if (!hasAnyPending) processingProductId = null;\n    });\n\n    for (final d in list) {\n      switch (d.status) {\n        case PurchaseStatus.purchased:\n        case PurchaseStatus.restored:\n          if (d.pendingCompletePurchase) {\n            InAppPurchase.instance.completePurchase(d);\n          }\n          setState(() {\n            purchasedProductIds.add(d.productID);\n          });\n          break;\n\n        case PurchaseStatus.pending:\n          if (processingProductId == null) {\n            setState(() {\n              processingProductId = d.productID;\n            });\n          }\n          break;\n\n        case PurchaseStatus.error:\n        case PurchaseStatus.canceled:\n          if (d.pendingCompletePurchase) {\n            InAppPurchase.instance.completePurchase(d);\n          }\n          break;\n      }\n    }\n  }\n\n  Widget _buildHeader() {\n    return Container(\n      margin: const .all(16),\n      padding: const .all(20),\n      decoration: BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            context.colors.primaryContainer.withOpacity(0.5),\n            context.colors.tertiaryContainer.withOpacity(0.5),\n          ],\n          begin: .topLeft,\n          end: .bottomRight,\n        ),\n        borderRadius: .circular(20),\n      ),\n      child: Column(\n        children: [\n          Icon(\n            Symbols.favorite_rounded,\n            size: 48,\n            color: context.colors.primary,\n          ),\n          const SizedBox(height: 12),\n          Text(\n            '支持 DPIP'.i18n,\n            style: context.texts.headlineSmall?.copyWith(\n              fontWeight: .bold,\n              color: context.colors.onSurface,\n            ),\n          ),\n          const SizedBox(height: 8),\n          Text(\n            'DPIP 作為一款致力於提供即時地震資訊的 App，目前並無廣告或其他盈利模式。您的支持將幫助我們維持伺服器運行與持續開發。'.i18n,\n            style: context.texts.bodyMedium?.copyWith(\n              color: context.colors.onSurfaceVariant,\n            ),\n            textAlign: .center,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildSubscriptionSection(List<ProductDetails> subscriptions) {\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        Padding(\n          padding: const .fromLTRB(16, 16, 16, 8),\n          child: Row(\n            children: [\n              Icon(\n                Symbols.workspace_premium_rounded,\n                color: const Color(0xFFFFD700),\n                size: 24,\n              ),\n              const SizedBox(width: 8),\n              Text(\n                '訂閱制'.i18n,\n                style: context.texts.titleMedium?.copyWith(\n                  fontWeight: .bold,\n                  color: context.colors.onSurface,\n                ),\n              ),\n              const Spacer(),\n              Container(\n                padding: const .symmetric(\n                  horizontal: 8,\n                  vertical: 4,\n                ),\n                decoration: BoxDecoration(\n                  gradient: const LinearGradient(\n                    colors: [Color(0xFFFFD700), Color(0xFFFFA500)],\n                  ),\n                  borderRadius: .circular(12),\n                ),\n                child: Text(\n                  '推薦'.i18n,\n                  style: context.texts.labelSmall?.copyWith(\n                    color: Colors.black87,\n                    fontWeight: .bold,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n        for (final product in subscriptions) _buildSubscriptionCard(product),\n      ],\n    );\n  }\n\n  Widget _buildSubscriptionCard(ProductDetails product) {\n    final isDisabled = processingProductId != null && processingProductId != product.id;\n    final isProcessing = processingProductId == product.id;\n    final isPurchased = purchasedProductIds.contains(product.id);\n\n    final title = product.title.contains('(')\n        ? product.title.substring(0, product.title.indexOf('(')).trim()\n        : product.title;\n\n    return AnimatedBuilder(\n      animation: _shimmerController,\n      builder: (context, child) {\n        return Container(\n          margin: const .symmetric(horizontal: 16, vertical: 6),\n          decoration: BoxDecoration(\n            borderRadius: .circular(16),\n            gradient: LinearGradient(\n              colors: [\n                const Color(0xFFFFD700).withOpacity(isDisabled ? 0.3 : 0.15),\n                const Color(0xFFFFA500).withOpacity(isDisabled ? 0.3 : 0.15),\n                const Color(0xFFFFD700).withOpacity(isDisabled ? 0.3 : 0.15),\n              ],\n              stops: [\n                0.0,\n                _shimmerController.value,\n                1.0,\n              ],\n              begin: .topLeft,\n              end: .bottomRight,\n            ),\n            boxShadow: isDisabled\n                ? null\n                : [\n                    BoxShadow(\n                      color: const Color(0xFFFFD700).withOpacity(0.2),\n                      blurRadius: 8,\n                      offset: const Offset(0, 2),\n                    ),\n                  ],\n          ),\n          child: Material(\n            color: Colors.transparent,\n            child: InkWell(\n              borderRadius: .circular(16),\n              onTap: isPending || isPurchased\n                  ? null\n                  : () {\n                      setState(() {\n                        isPending = true;\n                        processingProductId = product.id;\n                      });\n                      final purchaseParam = PurchaseParam(\n                        productDetails: product,\n                      );\n                      InAppPurchase.instance.buyNonConsumable(\n                        purchaseParam: purchaseParam,\n                      );\n                    },\n              child: Padding(\n                padding: const .all(16),\n                child: Row(\n                  children: [\n                    Container(\n                      padding: const .all(10),\n                      decoration: BoxDecoration(\n                        gradient: LinearGradient(\n                          colors: [\n                            const Color(\n                              0xFFFFD700,\n                            ).withOpacity(isDisabled ? 0.3 : 0.8),\n                            const Color(\n                              0xFFFFA500,\n                            ).withOpacity(isDisabled ? 0.3 : 0.8),\n                          ],\n                          begin: .topLeft,\n                          end: .bottomRight,\n                        ),\n                        borderRadius: .circular(12),\n                      ),\n                      child: Icon(\n                        Symbols.diamond_rounded,\n                        color: isDisabled ? Colors.grey : Colors.white,\n                        size: 24,\n                      ),\n                    ),\n                    const SizedBox(width: 16),\n                    Expanded(\n                      child: Column(\n                        crossAxisAlignment: .start,\n                        children: [\n                          Text(\n                            title,\n                            style: context.texts.titleMedium?.copyWith(\n                              fontWeight: .bold,\n                              color: isDisabled\n                                  ? context.theme.disabledColor\n                                  : context.colors.onSurface,\n                            ),\n                          ),\n                          const SizedBox(height: 4),\n                          Text(\n                            product.description,\n                            style: context.texts.bodySmall?.copyWith(\n                              color: isDisabled\n                                  ? context.theme.disabledColor\n                                  : context.colors.onSurfaceVariant,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                    if (isPurchased)\n                      Container(\n                        padding: const .all(8),\n                        decoration: BoxDecoration(\n                          color: context.colors.primary,\n                          shape: .circle,\n                        ),\n                        child: Icon(\n                          Symbols.check_rounded,\n                          color: context.colors.onPrimary,\n                          size: 20,\n                        ),\n                      )\n                    else if (isProcessing)\n                      const SizedBox(\n                        width: 24,\n                        height: 24,\n                        child: CircularProgressIndicator(strokeWidth: 2),\n                      )\n                    else\n                      Container(\n                        padding: const .symmetric(\n                          horizontal: 12,\n                          vertical: 6,\n                        ),\n                        decoration: BoxDecoration(\n                          gradient: isDisabled\n                              ? null\n                              : const LinearGradient(\n                                  colors: [\n                                    Color(0xFFFFD700),\n                                    Color(0xFFFFA500),\n                                  ],\n                                ),\n                          color: isDisabled ? context.theme.disabledColor : null,\n                          borderRadius: .circular(20),\n                        ),\n                        child: Text(\n                          '{price}/月'.i18n.args({'price': product.price}),\n                          style: context.texts.labelLarge?.copyWith(\n                            color: isDisabled ? Colors.white54 : Colors.black87,\n                            fontWeight: .bold,\n                          ),\n                        ),\n                      ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildOneTimeSection(List<ProductDetails> oneTime) {\n    return Column(\n      crossAxisAlignment: .start,\n      children: [\n        Padding(\n          padding: const .fromLTRB(16, 24, 16, 8),\n          child: Row(\n            children: [\n              Icon(\n                Symbols.favorite_rounded,\n                color: context.colors.primary,\n                size: 24,\n              ),\n              const SizedBox(width: 8),\n              Text(\n                '單次支援'.i18n,\n                style: context.texts.titleMedium?.copyWith(\n                  fontWeight: .bold,\n                  color: context.colors.onSurface,\n                ),\n              ),\n            ],\n          ),\n        ),\n        for (final product in oneTime) _buildOneTimeCard(product),\n      ],\n    );\n  }\n\n  Widget _buildOneTimeCard(ProductDetails product) {\n    final isDisabled = processingProductId != null && processingProductId != product.id;\n    final isProcessing = processingProductId == product.id;\n\n    final title = product.title.contains('(')\n        ? product.title.substring(0, product.title.indexOf('(')).trim()\n        : product.title;\n\n    return Container(\n      margin: const .symmetric(horizontal: 16, vertical: 6),\n      decoration: BoxDecoration(\n        color: context.colors.surfaceContainerLow,\n        borderRadius: .circular(16),\n      ),\n      child: Material(\n        color: Colors.transparent,\n        child: InkWell(\n          borderRadius: .circular(16),\n          onTap: isPending\n              ? null\n              : () {\n                  setState(() {\n                    isPending = true;\n                    processingProductId = product.id;\n                  });\n                  final purchaseParam = PurchaseParam(\n                    productDetails: product,\n                  );\n                  InAppPurchase.instance.buyConsumable(\n                    purchaseParam: purchaseParam,\n                  );\n                },\n          child: Padding(\n            padding: const .all(16),\n            child: Row(\n              children: [\n                Container(\n                  padding: const .all(10),\n                  decoration: BoxDecoration(\n                    color: context.colors.primaryContainer,\n                    borderRadius: .circular(12),\n                  ),\n                  child: Icon(\n                    Symbols.coffee_rounded,\n                    color: isDisabled\n                        ? context.theme.disabledColor\n                        : context.colors.onPrimaryContainer,\n                    size: 24,\n                  ),\n                ),\n                const SizedBox(width: 16),\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: .start,\n                    children: [\n                      Text(\n                        title,\n                        style: context.texts.titleMedium?.copyWith(\n                          fontWeight: .bold,\n                          color: isDisabled\n                              ? context.theme.disabledColor\n                              : context.colors.onSurface,\n                        ),\n                      ),\n                      const SizedBox(height: 4),\n                      Text(\n                        product.description,\n                        style: context.texts.bodySmall?.copyWith(\n                          color: isDisabled\n                              ? context.theme.disabledColor\n                              : context.colors.onSurfaceVariant,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                if (isProcessing)\n                  const SizedBox(\n                    width: 24,\n                    height: 24,\n                    child: CircularProgressIndicator(strokeWidth: 2),\n                  )\n                else\n                  Container(\n                    padding: const .symmetric(\n                      horizontal: 12,\n                      vertical: 6,\n                    ),\n                    decoration: BoxDecoration(\n                      color: isDisabled ? context.theme.disabledColor : context.colors.primary,\n                      borderRadius: .circular(20),\n                    ),\n                    child: Text(\n                      product.price,\n                      style: context.texts.labelLarge?.copyWith(\n                        color: isDisabled ? Colors.white54 : context.colors.onPrimary,\n                        fontWeight: .bold,\n                      ),\n                    ),\n                  ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFooter() {\n    return Padding(\n      padding: const .all(24),\n      child: Column(\n        children: [\n          Divider(color: context.colors.outlineVariant),\n          const SizedBox(height: 16),\n          Wrap(\n            spacing: 24,\n            runSpacing: 8,\n            alignment: .center,\n            children: [\n              _buildFooterLink(\n                '恢復購買'.i18n,\n                onTap: () async {\n                  final bool available = await InAppPurchase.instance.isAvailable();\n                  if (!context.mounted) return;\n\n                  if (!available) {\n                    final storeName = Platform.isIOS ? 'App Store' : 'Google Play';\n                    context.scaffoldMessenger.showSnackBar(\n                      SnackBar(\n                        content: Text(\n                          '無法連線至 {store}，請稍後再試。'.i18n.args({\n                            'store': storeName,\n                          }),\n                        ),\n                      ),\n                    );\n                    return;\n                  }\n                  InAppPurchase.instance.restorePurchases();\n\n                  context.scaffoldMessenger.showSnackBar(\n                    SnackBar(content: Text('正在恢復您購買的訂閱'.i18n)),\n                  );\n                },\n              ),\n              _buildFooterLink(\n                '使用條款'.i18n,\n                onTap: () => launchUrl(Uri.parse('https://exptech.dev/tos')),\n              ),\n              _buildFooterLink(\n                '隱私權政策'.i18n,\n                onTap: () => launchUrl(Uri.parse('https://exptech.dev/privacy')),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFooterLink(String text, {required VoidCallback onTap}) {\n    return InkWell(\n      onTap: onTap,\n      borderRadius: .circular(4),\n      child: Padding(\n        padding: const .symmetric(horizontal: 4, vertical: 2),\n        child: Text(\n          text,\n          style: context.texts.bodySmall?.copyWith(\n            color: context.colors.primary,\n            decoration: TextDecoration.underline,\n            decorationColor: context.colors.primary,\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _shimmerController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 2000),\n    )..repeat();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!mounted) return;\n      _refreshIndicatorKey.currentState?.show();\n    });\n\n    subscription?.cancel();\n    subscription = InAppPurchase.instance.purchaseStream.listen(\n      onPurchaseUpdate,\n      onError: (error) {\n        if (!mounted) return;\n        setState(() => isPending = false);\n      },\n    );\n    refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      key: _refreshIndicatorKey,\n      onRefresh: refresh,\n      child: FutureBuilder(\n        future: products.future,\n        builder: (context, snapshot) {\n          final data = snapshot.data;\n          final error = snapshot.error;\n\n          if (error != null) {\n            return Center(\n              child: Column(\n                mainAxisAlignment: .center,\n                spacing: 16,\n                children: [\n                  Text(error.toString()),\n                  FilledButton.tonal(\n                    onPressed: refresh,\n                    child: Text('重新載入'.i18n),\n                  ),\n                ],\n              ),\n            );\n          }\n\n          if (data == null) {\n            return Center(\n              child: Column(\n                mainAxisAlignment: .center,\n                spacing: 16,\n                children: [\n                  const CircularProgressIndicator(),\n                  Text('正在載入商店物品中'.i18n),\n                ],\n              ),\n            );\n          }\n\n          final subscriptions = data\n              .where((item) => item.isSubscription)\n              .sorted((a, b) => ascending(a.rawPrice, b.rawPrice));\n          final oneTime = data\n              .where((item) => !item.isSubscription)\n              .sorted((a, b) => ascending(a.rawPrice, b.rawPrice));\n\n          return ListView(\n            padding: .only(\n              bottom: context.padding.bottom + 16,\n            ),\n            children: [\n              _buildHeader(),\n              if (subscriptions.isNotEmpty) _buildSubscriptionSection(subscriptions),\n              if (oneTime.isNotEmpty) _buildOneTimeSection(oneTime),\n              _buildFooter(),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _shimmerController.dispose();\n    subscription?.cancel();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/experimental/page.dart",
    "content": "/// Experimental features settings page.\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A page for toggling experimental (in-development) features.\n///\n/// Each feature shows a confirmation dialog with a countdown before it can be\n/// enabled. Disabled features can be turned off immediately.\nclass SettingsExperimentalPage extends StatefulWidget {\n  /// Creates a [SettingsExperimentalPage].\n  const SettingsExperimentalPage({super.key});\n\n  @override\n  State<SettingsExperimentalPage> createState() => _SettingsExperimentalPageState();\n}\n\nclass _SettingsExperimentalPageState extends State<SettingsExperimentalPage> {\n  bool _launchToMonitor = Preference.experimentalLaunchToMonitor ?? false;\n  bool _eewAllSource = Preference.experimentalEewAllSource ?? false;\n\n  Future<void> _showEnableWarningDialog({\n    required String featureName,\n    required VoidCallback onConfirm,\n  }) async {\n    final confirmed = await showDialog<bool>(\n      context: context,\n      barrierDismissible: false,\n      builder: (context) => _ExperimentalWarningDialog(featureName: featureName),\n    );\n\n    if (confirmed == true) {\n      onConfirm();\n    }\n  }\n\n  void _toggleEewAllSource(bool value) {\n    if (value) {\n      _showEnableWarningDialog(\n        featureName: '地震速報不限制非 CWA 來源'.i18n,\n        onConfirm: () {\n          setState(() => _eewAllSource = true);\n          Preference.experimentalEewAllSource = true;\n        },\n      );\n    } else {\n      setState(() => _eewAllSource = false);\n      Preference.experimentalEewAllSource = false;\n    }\n  }\n\n  void _toggleLaunchToMonitor(bool value) {\n    if (value) {\n      _showEnableWarningDialog(\n        featureName: '啟動時進入強震監視器'.i18n,\n        onConfirm: () {\n          setState(() => _launchToMonitor = true);\n          Preference.experimentalLaunchToMonitor = true;\n        },\n      );\n    } else {\n      setState(() => _launchToMonitor = false);\n      Preference.experimentalLaunchToMonitor = false;\n    }\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    return Padding(\n      padding: const .fromLTRB(16, 8, 16, 0),\n      child: Row(\n        children: [\n          Container(\n            padding: const .all(10),\n            decoration: BoxDecoration(\n              color: context.colors.primaryContainer,\n              borderRadius: .circular(12),\n            ),\n            child: Icon(\n              Symbols.science_rounded,\n              color: context.colors.onPrimaryContainer,\n              size: 24,\n            ),\n          ),\n          const SizedBox(width: 12),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: .start,\n              children: [\n                Text(\n                  '實驗性功能'.i18n,\n                  style: context.texts.titleLarge?.copyWith(\n                    fontWeight: .bold,\n                  ),\n                ),\n                Text(\n                  '搶先體驗開發中的新功能'.i18n,\n                  style: context.texts.bodySmall?.copyWith(\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildIconContainer({\n    required IconData icon,\n    required Color color,\n  }) {\n    return Container(\n      padding: const .all(8),\n      decoration: BoxDecoration(\n        color: color.withValues(alpha: 0.15),\n        borderRadius: .circular(10),\n      ),\n      child: Icon(icon, color: color, size: 20),\n    );\n  }\n\n  Widget _buildWarningCard(BuildContext context) {\n    return Container(\n      margin: const .fromLTRB(16, 16, 16, 0),\n      padding: const .all(16),\n      decoration: BoxDecoration(\n        color: Colors.amber.withValues(alpha: 0.15),\n        borderRadius: .circular(16),\n      ),\n      child: Row(\n        crossAxisAlignment: .start,\n        children: [\n          Container(\n            padding: const .all(10),\n            decoration: BoxDecoration(\n              color: Colors.amber.withValues(alpha: 0.2),\n              borderRadius: .circular(12),\n            ),\n            child: Icon(\n              Symbols.warning_rounded,\n              color: Colors.amber[700],\n              size: 24,\n            ),\n          ),\n          const SizedBox(width: 16),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: .start,\n              children: [\n                Text(\n                  '注意'.i18n,\n                  style: context.texts.titleMedium?.copyWith(\n                    fontWeight: .w600,\n                    color: Colors.amber[700],\n                  ),\n                ),\n                const SizedBox(height: 4),\n                Text(\n                  '這些功能仍在開發中，可能會不穩定或在未來的版本中變更。'.i18n,\n                  style: context.texts.bodySmall?.copyWith(\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: .only(\n        top: 8,\n        bottom: 16 + context.padding.bottom,\n      ),\n      children: [\n        _buildHeader(context),\n        _buildWarningCard(context),\n\n        // 啟動行為\n        SegmentedList(\n          label: Text('啟動行為'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: true,\n              leading: _buildIconContainer(\n                icon: Symbols.monitor_heart_rounded,\n                color: Colors.red,\n              ),\n              title: Text('啟動時進入強震監視器'.i18n),\n              subtitle: Text('開啟 App 時直接進入強震監視器地圖'.i18n),\n              trailing: Switch(\n                value: _launchToMonitor,\n                onChanged: _toggleLaunchToMonitor,\n              ),\n              onTap: () => _toggleLaunchToMonitor(!_launchToMonitor),\n            ),\n          ],\n        ),\n\n        SegmentedList(\n          label: Text('地震速報'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: true,\n              leading: _buildIconContainer(\n                icon: Symbols.earthquake_rounded,\n                color: Colors.orange,\n              ),\n              title: Text('不限制非 CWA 來源'.i18n),\n              subtitle: Text('顯示所有來源的地震速報資料'.i18n),\n              trailing: Switch(\n                value: _eewAllSource,\n                onChanged: _toggleEewAllSource,\n              ),\n              onTap: () => _toggleEewAllSource(!_eewAllSource),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n\n/// A confirmation dialog with a countdown timer before the confirm button\n/// becomes enabled.\nclass _ExperimentalWarningDialog extends StatefulWidget {\n  /// The display name of the experimental feature being enabled.\n  final String featureName;\n\n  const _ExperimentalWarningDialog({required this.featureName});\n\n  @override\n  State<_ExperimentalWarningDialog> createState() => _ExperimentalWarningDialogState();\n}\n\nclass _ExperimentalWarningDialogState extends State<_ExperimentalWarningDialog> {\n  int _countdown = 5;\n  Timer? _timer;\n\n  void _startCountdown() {\n    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {\n      if (_countdown > 0) {\n        setState(() => _countdown--);\n      } else {\n        timer.cancel();\n      }\n    });\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _startCountdown();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final canConfirm = _countdown == 0;\n\n    return AlertDialog(\n      icon: Icon(\n        Symbols.warning_rounded,\n        color: Colors.amber[700],\n        size: 48,\n      ),\n      title: Text('啟用實驗性功能'.i18n),\n      content: Column(\n        mainAxisSize: .min,\n        crossAxisAlignment: .start,\n        children: [\n          Text(\n            '你即將啟用：'.i18n,\n            style: context.texts.bodyMedium,\n          ),\n          const SizedBox(height: 8),\n          Container(\n            padding: const .all(12),\n            decoration: BoxDecoration(\n              color: context.colors.surfaceContainerHighest,\n              borderRadius: .circular(8),\n            ),\n            child: Row(\n              children: [\n                Icon(\n                  Symbols.science_rounded,\n                  color: context.colors.primary,\n                  size: 20,\n                ),\n                const SizedBox(width: 8),\n                Expanded(\n                  child: Text(\n                    widget.featureName,\n                    style: context.texts.bodyMedium?.copyWith(\n                      fontWeight: .bold,\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n          const SizedBox(height: 16),\n          Text(\n            '此功能為實驗性質，可能會造成應用程式不穩定或行為異常。如遇問題，請至設定中關閉此功能。'.i18n,\n            style: context.texts.bodySmall?.copyWith(\n              color: context.colors.onSurfaceVariant,\n            ),\n          ),\n        ],\n      ),\n      actions: [\n        TextButton(\n          onPressed: () => context.pop(false),\n          child: Text('取消'.i18n),\n        ),\n        FilledButton(\n          onPressed: canConfirm ? () => context.pop(true) : null,\n          child: Text(\n            canConfirm ? '啟用'.i18n : '${_countdown}s',\n          ),\n        ),\n      ],\n    );\n  }\n\n  @override\n  void dispose() {\n    _timer?.cancel();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/layout/page.dart",
    "content": "/// Home layout settings page for reordering and toggling displayed sections.\nlibrary;\n\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\n\n/// A page that lets users reorder and enable or disable home screen sections.\n///\n/// Drag tiles to reorder enabled sections, or tap the add/remove buttons to\n/// toggle sections on and off. Requires [SettingsUserInterfaceModel] in the\n/// widget tree.\nclass SettingsLayoutPage extends StatelessWidget {\n  /// Creates a [SettingsLayoutPage].\n  const SettingsLayoutPage({super.key});\n\n  static const _sectionInfo = {\n    HomeDisplaySection.radar: (\n      icon: Symbols.radar_rounded,\n      color: Colors.blueAccent,\n      title: '雷達回波',\n      subtitle: '顯示即時雷達回波圖',\n    ),\n    HomeDisplaySection.forecast: (\n      icon: Symbols.sunny_rounded,\n      color: Colors.orangeAccent,\n      title: '天氣預報',\n      subtitle: '顯示未來 24 小時的天氣預報',\n    ),\n    HomeDisplaySection.wind: (\n      icon: Symbols.wind_power_rounded,\n      color: Colors.tealAccent,\n      title: '風向',\n      subtitle: '顯示風向與風力級數',\n    ),\n  };\n\n  Widget _buildSectionTile(\n    BuildContext context, {\n    required HomeDisplaySection section,\n    required IconData icon,\n    required Color color,\n    required String title,\n    required String subtitle,\n    required bool isFirst,\n    required bool isLast,\n    required int index,\n  }) {\n    return Material(\n      color: Colors.transparent,\n      child: Container(\n        margin: .only(bottom: isLast ? 0 : 2),\n        decoration: BoxDecoration(\n          color: context.colors.surfaceContainerLow,\n          borderRadius: .vertical(\n            top: isFirst ? const Radius.circular(12) : Radius.zero,\n            bottom: isLast ? const Radius.circular(12) : Radius.zero,\n          ),\n        ),\n        child: ListTile(\n          contentPadding: const .symmetric(\n            horizontal: 16,\n            vertical: 4,\n          ),\n          leading: Container(\n            padding: const .all(8),\n            decoration: BoxDecoration(\n              color: color.withValues(alpha: 0.15),\n              borderRadius: .circular(8),\n            ),\n            child: Icon(icon, color: color, size: 20),\n          ),\n          title: Text(title),\n          subtitle: Text(subtitle, style: context.texts.bodySmall),\n          trailing: Row(\n            mainAxisSize: .min,\n            children: [\n              IconButton(\n                icon: Icon(\n                  Symbols.remove_circle_outline_rounded,\n                  color: context.colors.error,\n                ),\n                onPressed: () {\n                  context.userInterface.toggleSection(section, false);\n                },\n                tooltip: '停用'.i18n,\n              ),\n              ReorderableDragStartListener(\n                index: index,\n                child: Padding(\n                  padding: const .all(8),\n                  child: Icon(\n                    Symbols.drag_handle_rounded,\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDisabledTile(\n    BuildContext context, {\n    required HomeDisplaySection section,\n    required IconData icon,\n    required Color color,\n    required String title,\n    required String subtitle,\n    required bool isFirst,\n    required bool isLast,\n  }) {\n    return Container(\n      margin: .only(bottom: isLast ? 0 : 2),\n      decoration: BoxDecoration(\n        color: context.colors.surfaceContainerLow.withValues(alpha: 0.5),\n        borderRadius: .vertical(\n          top: isFirst ? const Radius.circular(12) : Radius.zero,\n          bottom: isLast ? const Radius.circular(12) : Radius.zero,\n        ),\n      ),\n      child: ListTile(\n        contentPadding: const .symmetric(\n          horizontal: 16,\n          vertical: 4,\n        ),\n        leading: Container(\n          padding: const .all(8),\n          decoration: BoxDecoration(\n            color: context.colors.surfaceContainerHighest,\n            borderRadius: .circular(8),\n          ),\n          child: Icon(\n            icon,\n            color: context.colors.onSurfaceVariant.withValues(alpha: 0.5),\n            size: 20,\n          ),\n        ),\n        title: Text(\n          title,\n          style: TextStyle(color: context.colors.onSurfaceVariant),\n        ),\n        subtitle: Text(\n          subtitle,\n          style: context.texts.bodySmall?.copyWith(\n            color: context.colors.onSurfaceVariant.withValues(alpha: 0.7),\n          ),\n        ),\n        trailing: IconButton(\n          icon: Icon(\n            Symbols.add_circle_outline_rounded,\n            color: context.colors.primary,\n          ),\n          onPressed: () {\n            context.userInterface.toggleSection(section, true);\n          },\n          tooltip: '啟用'.i18n,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Selector<SettingsUserInterfaceModel, List<HomeDisplaySection>>(\n      selector: (context, model) => List.from(model.homeSections),\n      builder: (context, sections, child) {\n        final disabledSections = HomeDisplaySection.values\n            .where((s) => !sections.contains(s))\n            .toList();\n\n        return CustomScrollView(\n          slivers: [\n            SliverToBoxAdapter(\n              child: SettingsHeader(\n                icon: Symbols.dashboard_rounded,\n                title: Text('版面'.i18n),\n                subtitle: Text('調整首頁的版面樣式'.i18n),\n              ),\n            ),\n            if (sections.isNotEmpty) ...[\n              SliverPadding(\n                padding: const .fromLTRB(16, 16, 16, 8),\n                sliver: SliverToBoxAdapter(\n                  child: Text(\n                    '拖曳調整順序'.i18n,\n                    style: context.texts.labelMedium?.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                ),\n              ),\n              SliverPadding(\n                padding: const .symmetric(horizontal: 16),\n                sliver: SliverReorderableList(\n                  itemCount: sections.length,\n                  onReorderItem: (oldIndex, newIndex) {\n                    context.userInterface.reorderSection(oldIndex, newIndex);\n                  },\n                  itemBuilder: (context, index) {\n                    final section = sections[index];\n                    final info = _sectionInfo[section]!;\n                    final isFirst = index == 0;\n                    final isLast = index == sections.length - 1;\n\n                    return ReorderableDelayedDragStartListener(\n                      key: ValueKey(section),\n                      index: index,\n                      child: _buildSectionTile(\n                        context,\n                        section: section,\n                        icon: info.icon,\n                        color: info.color,\n                        title: info.title.i18n,\n                        subtitle: info.subtitle.i18n,\n                        isFirst: isFirst,\n                        isLast: isLast,\n                        index: index,\n                      ),\n                    );\n                  },\n                ),\n              ),\n            ],\n            if (disabledSections.isNotEmpty) ...[\n              SliverPadding(\n                padding: const .fromLTRB(16, 16, 16, 8),\n                sliver: SliverToBoxAdapter(\n                  child: Text(\n                    '已停用'.i18n,\n                    style: context.texts.labelMedium?.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                ),\n              ),\n              SliverPadding(\n                padding: const .symmetric(horizontal: 16),\n                sliver: SliverList(\n                  delegate: SliverChildBuilderDelegate(\n                    (context, index) {\n                      final section = disabledSections[index];\n                      final info = _sectionInfo[section]!;\n\n                      return _buildDisabledTile(\n                        context,\n                        section: section,\n                        icon: info.icon,\n                        color: info.color,\n                        title: info.title.i18n,\n                        subtitle: info.subtitle.i18n,\n                        isFirst: index == 0,\n                        isLast: index == disabledSections.length - 1,\n                      );\n                    },\n                    childCount: disabledSections.length,\n                  ),\n                ),\n              ),\n            ] else ...[\n              SliverPadding(\n                padding: const .fromLTRB(16, 16, 16, 8),\n                sliver: SliverToBoxAdapter(\n                  child: Text(\n                    '所有區塊皆已啟用'.i18n,\n                    style: context.texts.bodySmall?.copyWith(\n                      color: context.colors.onSurfaceVariant,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n            SliverPadding(\n              padding: .only(\n                bottom: context.padding.bottom + 16,\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/layout.dart",
    "content": "/// Shared scaffold layout for all settings pages.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\n/// A scaffold wrapper for settings pages.\n///\n/// Provides a consistent app bar with a back button and applies the\n/// fade-forward page transition theme to its [child]:\n///\n/// ```dart\n/// SettingsLayout(\n///   child: MySettingsPage(),\n/// )\n/// ```\nclass SettingsLayout extends StatefulWidget {\n  /// The settings page content to display inside the scaffold body.\n  final Widget child;\n\n  /// Creates a [SettingsLayout].\n  const SettingsLayout({super.key, required this.child});\n\n  @override\n  State<SettingsLayout> createState() => _SettingsLayoutState();\n}\n\nclass _SettingsLayoutState extends State<SettingsLayout> {\n  final controller = ScrollController();\n\n  @override\n  Widget build(BuildContext context) {\n    return Theme(\n      data: context.theme.copyWith(\n        pageTransitionsTheme: kFadeForwardPageTransitionsTheme,\n      ),\n      child: Scaffold(\n        appBar: AppBar(\n          title: Text('設定'.i18n),\n          leading: BackButton(onPressed: () => context.pop()),\n          centerTitle: true,\n        ),\n        body: widget.child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/locale/page.dart",
    "content": "/// Locale settings page for adjusting the app display language.\nlibrary;\n\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/locale.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A settings page for selecting the app display language.\n///\n/// Shows the current locale and a link to open the locale selection page.\n/// Requires [SettingsUserInterfaceModel] in the widget tree.\nclass SettingsLocalePage extends StatelessWidget {\n  /// Creates a [SettingsLocalePage].\n  const SettingsLocalePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<SettingsUserInterfaceModel>(\n      builder: (context, model, child) {\n        return ListView(\n          padding: .only(\n            top: 16,\n            bottom: 16 + context.padding.bottom,\n          ),\n          children: [\n            SettingsHeader(\n              icon: Symbols.translate_rounded,\n              title: Text('語言'.i18n),\n              subtitle: Text('調整 DPIP 的顯示語言'.i18n),\n            ),\n            const SizedBox(height: 16),\n            SegmentedList(\n              children: [\n                SegmentedListTile(\n                  isFirst: true,\n                  leading: ContainedIcon(\n                    Symbols.translate_rounded,\n                    color: Colors.blueAccent,\n                  ),\n                  title: Text('顯示語言'.i18n),\n                  subtitle: Text(model.locale?.nativeName ?? '系統語言'.i18n),\n                  trailing: Icon(Symbols.chevron_right_rounded),\n                  onTap: () => const SettingsLocaleSelectRoute().push(context),\n                ),\n                SegmentedListTile(\n                  isLast: true,\n                  leading: ContainedIcon(\n                    Symbols.groups_rounded,\n                    color: Colors.greenAccent,\n                  ),\n                  title: Text('協助翻譯'.i18n),\n                  subtitle: Text('點擊這裡來幫助我們改進 DPIP 的翻譯'.i18n),\n                  trailing: Icon(Symbols.arrow_outward_rounded),\n                  onTap: () => 'https://crowdin.com/project/dpip'.launch(),\n                  onLongPress: () => 'https://crowdin.com/project/dpip'.copy(),\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/locale/select/page.dart",
    "content": "/// Locale selection page showing all supported languages with translation\n/// progress.\nlibrary;\n\nimport 'dart:math';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/model/crowdin/localization_progress.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/extensions/locale.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:intl/intl.dart';\nimport 'package:m3e_collection/m3e_collection.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:option_result/result.dart';\n\n/// A page for selecting the app display language from the list of supported\n/// locales.\n///\n/// Each locale row shows its Crowdin translation and approval percentage.\n/// Pull to refresh reloads the progress data.\nclass SettingsLocaleSelectPage extends StatefulWidget {\n  /// Creates a [SettingsLocaleSelectPage].\n  const SettingsLocaleSelectPage({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _SettingsLocaleSelectPageState();\n}\n\nclass _SettingsLocaleSelectPageState extends State<SettingsLocaleSelectPage>\n    with TickerProviderStateMixin {\n  late final _animationController =\n      AnimationController(\n          vsync: this,\n          duration: const Duration(milliseconds: 1200),\n        )\n        ..addListener(() {\n          if (mounted) setState(() {});\n        })\n        ..repeat();\n\n  Result<List<CrowdinLocalizationProgress>, String>? progress;\n  List<Locale> localeList = I18n.supportedLocales\n      .where((e) => !['zh'].contains(e.toLanguageTag()))\n      .toList();\n\n  Future<void> _refresh() async {\n    final result = await Global.api.getLocalizationProgress();\n    setState(() => progress = result);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final locale = context.useUserInterface.locale;\n    final data = switch (progress) {\n      Ok(:final value)? => value,\n      _ => null,\n    };\n\n    final phaseValue = _animationController.value * 2 * pi;\n\n    final children = localeList.mapIndexed((index, item) {\n      final p = data?.firstWhereOrNull(\n        (e) => e.id == item.toLanguageTag(),\n      );\n\n      final translated = p != null ? NumberFormat('#.#%').format(p.translation / 100) : '...';\n      final approved = p != null ? NumberFormat('#.#%').format(p.approval / 100) : '...';\n\n      final isSelected = item.toLanguageTag() == locale?.toLanguageTag();\n\n      final progressBar = IgnorePointer(\n        child: SizedBox(\n          height: 10,\n          child: switch (p) {\n            null => LinearProgressIndicatorM3E(\n              size: .s,\n              phase: phaseValue,\n            ),\n            _ => SliderTheme(\n              data: SliderThemeData(\n                thumbSize: .all(.zero),\n                trackGap: 1,\n                trackHeight: 5,\n                padding: .zero,\n                year2023: false,\n              ),\n              child: Slider(\n                activeColor: context.theme.extendedColors.green,\n                secondaryActiveColor: context.theme.extendedColors.blue,\n                value: p.approval / 100,\n                secondaryTrackValue: p.translation / 100,\n                onChanged: (_) {},\n              ),\n            ),\n          },\n        ),\n      );\n\n      return SegmentedListTile(\n        isFirst: index == 0,\n        isLast: index == localeList.length - 1,\n        title: Text(item.nativeName),\n        subtitle: (item.toLanguageTag() != 'zh-Hant')\n            ? Column(\n                crossAxisAlignment: .start,\n                children: [\n                  Text(\n                    '已翻譯 {translated}・已校對 {approved}'.i18n.args({\n                      'translated': translated,\n                      'approved': approved,\n                    }),\n                  ),\n                  RepaintBoundary(\n                    child: Padding(\n                      padding: const .symmetric(vertical: 4),\n                      child: progressBar,\n                    ),\n                  ),\n                ],\n              )\n            : Text('來源語言'.i18n),\n        leading: Container(\n          height: 28,\n          width: 40,\n          decoration: BoxDecoration(\n            color: context.colors.secondaryContainer,\n            borderRadius: .circular(6),\n          ),\n          child: Center(\n            child: Text(\n              item.iconLabel,\n              style: context.texts.labelLarge?.copyWith(\n                color: context.colors.onSecondaryContainer,\n                height: 1,\n              ),\n            ),\n          ),\n        ),\n        trailing: Icon(isSelected ? Symbols.check_rounded : null),\n        onTap: () {\n          context.locale = item;\n          context.userInterface.setLocale(item);\n          context.pop();\n        },\n      );\n    }).toList();\n\n    return ExpressiveRefreshIndicator.contained(\n      onRefresh: _refresh,\n      backgroundColor: context.colors.primaryContainer,\n      child: ListView(\n        padding: .only(\n          top: 8,\n          bottom: 16 + context.padding.bottom,\n        ),\n        children: [\n          SegmentedList(\n            label: Text('選擇語言'.i18n),\n            children: children,\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _animationController.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/location/page.dart",
    "content": "/// Location settings page for managing the user's saved locations and\n/// automatic GPS tracking.\nlibrary;\n\nimport 'dart:io';\n\nimport 'package:collection/collection.dart';\nimport 'package:disable_battery_optimization/disable_battery_optimization.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/core/service.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/toast.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:provider/provider.dart';\n\n/// A global reference to the location settings page state, used by external\n/// callers to trigger a permission status refresh.\nfinal stateSettingsLocationView = _SettingsLocationPageState();\n\n/// Callback type invoked when the user's position has been updated.\ntypedef PositionUpdateCallback = void Function();\n\n/// Settings page for configuring the user's current location.\n///\n/// Allows toggling automatic GPS-based location updates and managing a list of\n/// favorited locations. Requires [SettingsLocationModel] in the widget tree.\nclass SettingsLocationPage extends StatefulWidget {\n  /// Creates a [SettingsLocationPage].\n  const SettingsLocationPage({super.key});\n\n  @override\n  State<SettingsLocationPage> createState() => _SettingsLocationPageState();\n}\n\nclass _SettingsLocationPageState extends State<SettingsLocationPage> with WidgetsBindingObserver {\n  PermissionStatus? notificationPermission;\n  PermissionStatus? locationPermission;\n  PermissionStatus? locationAlwaysPermission;\n  bool autoStartPermission = true;\n  bool batteryOptimizationPermission = true;\n\n  Future<void> permissionStatusUpdate() async {\n    final values = await Future.wait([\n      Permission.notification.status,\n      Permission.location.status,\n      Permission.locationAlways.status,\n      // if (Platform.isAndroid) Autostarter.checkAutoStartState(),\n      if (Platform.isAndroid) DisableBatteryOptimization.isBatteryOptimizationDisabled,\n    ]);\n\n    if (!mounted) return;\n\n    setState(() {\n      notificationPermission = values[0] as PermissionStatus?;\n      locationPermission = values[1] as PermissionStatus?;\n      locationAlwaysPermission = values[2] as PermissionStatus?;\n      autoStartPermission = true;\n      batteryOptimizationPermission = !Platform.isAndroid || (values[3] as bool? ?? true);\n    });\n  }\n\n  /// Shows an error dialog explaining why [type] permission is needed.\n  ///\n  /// [type] must be either a [Permission] or the string `'auto-start'` or\n  /// `'battery-optimization'`.\n  Future<void> showPermissionDialog(dynamic type) async {\n    if (!mounted) return;\n    if (type is! Permission && type is! String) return;\n\n    final title = switch (type) {\n      Permission.notification => '無法取得通知權限'.i18n,\n      Permission.location => '無法取得位置權限'.i18n,\n      Permission.locationAlways => '無法取得位置權限'.i18n,\n      'auto-start' => '無法取得自啟動權限'.i18n,\n      'battery-optimization' => '省電策略'.i18n,\n      _ => '無法取得權限'.i18n,\n    };\n\n    final content = switch (type) {\n      Permission.notification => '自動定位功能需要您允許 DPIP 使用通知權限才能正常運作。請您到應用程式設定中找到並允許「通知」權限後再試一次。'.i18n,\n      Permission.location => '自動定位功能需要您允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到並允許「位置」權限後再試一次。'.i18n,\n      Permission.locationAlways =>\n        Platform.isIOS\n            ? '自動定位功能需要您永遠允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「永遠」後再試一次。'.i18n\n            : '自動定位功能需要您一律允許 DPIP 使用位置權限才能正常運作。請您到應用程式設定中找到位置權限設定並選擇「一律允許」後再試一次。'.i18n,\n      'auto-start' => '為了獲得更好的自動定位體驗，您需要給予「自啟動權限」以便讓 DPIP 在背景自動設定所在地資訊。'.i18n,\n      'battery-optimization' => '為了獲得更好的自動定位體驗，您需要給予「無限制」以便讓 DPIP 在背景自動設定所在地資訊。'.i18n,\n      _ => '自動定位功能需要您允許 DPIP 使用權限才能正常運作。請您到應用程式設定中找到並允許「權限」後再試一次。'.i18n,\n    };\n\n    await showDialog(\n      context: context,\n      builder: (context) {\n        return AlertDialog(\n          icon: const Icon(Symbols.error_rounded),\n          title: Text(title),\n          content: Text(content),\n          actionsAlignment: .spaceBetween,\n          actions: [\n            TextButton(\n              child: Text('取消'.i18n),\n              onPressed: () {\n                context.navigator.pop();\n              },\n            ),\n            FilledButton(\n              child: Text('設定'.i18n),\n              onPressed: () {\n                openAppSettings();\n                context.navigator.pop();\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Future<bool> requestPermissions() async {\n    if (!await Permission.notification.request().isGranted) {\n      TalkerManager.instance.warning(\n        '🧪 failed notification (NOTIFICATION) permission test',\n      );\n      await showPermissionDialog(Permission.notification);\n      return false;\n    }\n\n    if (!await Permission.location.request().isGranted) {\n      TalkerManager.instance.warning(\n        '🧪 failed location (ACCESS_COARSE_LOCATION) permission test',\n      );\n      showPermissionDialog(Permission.location);\n      return false;\n    }\n\n    if (!await Permission.locationWhenInUse.request().isGranted) {\n      TalkerManager.instance.warning(\n        '🧪 failed location when in use (ACCESS_FINE_LOCATION) permission test',\n      );\n      showPermissionDialog(Permission.locationWhenInUse);\n      return false;\n    }\n\n    if (!await Permission.locationAlways.request().isGranted) {\n      TalkerManager.instance.warning(\n        '🧪 failed location always (ACCESS_BACKGROUND_LOCATION) permission test',\n      );\n      showPermissionDialog(Permission.locationAlways);\n      return false;\n    }\n\n    if (!Platform.isAndroid) return true;\n\n    autoStart:\n    {\n      // final available = await Autostarter.isAutoStartPermissionAvailable();\n      // if (available == null) break autoStart;\n\n      final status = await DisableBatteryOptimization.isAutoStartEnabled;\n      if (status == null || status) {\n        batteryOptimizationPermission = true;\n        break autoStart;\n      }\n\n      await DisableBatteryOptimization.showEnableAutoStartSettings(\n        '自動啟動'.i18n,\n        '為了獲得更好的 DPIP 體驗，請依照步驟啟用自動啟動功能，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。'.i18n,\n      );\n    }\n\n    batteryOptimization:\n    {\n      final status = await DisableBatteryOptimization.isBatteryOptimizationDisabled;\n      if (status == null || status) {\n        batteryOptimizationPermission = true;\n        break batteryOptimization;\n      }\n\n      await DisableBatteryOptimization.showDisableBatteryOptimizationSettings();\n    }\n\n    manufacturerBatteryOptimization:\n    {\n      final status = await DisableBatteryOptimization.isManufacturerBatteryOptimizationDisabled;\n      if (status == null || status) break manufacturerBatteryOptimization;\n\n      await DisableBatteryOptimization.showEnableAutoStartSettings(\n        '省電策略'.i18n,\n        '為了獲得更好的 DPIP 體驗，請依照步驟關閉省電策略，以便讓 DPIP 在背景能正常接收資訊以及更新所在地。'.i18n,\n      );\n    }\n\n    setState(() {});\n    return true;\n  }\n\n  Future toggleAutoLocation(bool shouldEnable) async {\n    if (shouldEnable) {\n      if (!await requestPermissions()) return;\n\n      GlobalProviders.location.setAuto(shouldEnable);\n      await LocationServiceManager.initalize();\n    } else {\n      await LocationServiceManager.stop();\n      GlobalProviders.location.setAuto(shouldEnable);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    permissionStatusUpdate();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    permissionStatusUpdate();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final permissionType = Platform.isAndroid ? '一律允許'.i18n : '永遠'.i18n;\n\n    return ListView(\n      padding: .only(top: 8, bottom: 16 + context.padding.bottom),\n      children: [\n        SettingsHeader(\n          icon: Symbols.pin_drop_rounded,\n          title: Text('所在地'.i18n),\n          subtitle: Text('設定你的所在地來接收當地的即時資訊'.i18n),\n        ),\n        const SizedBox(height: 16),\n        SegmentedList(\n          children: [\n            Selector<SettingsLocationModel, bool>(\n              selector: (context, model) => model.auto,\n              builder: (context, auto, child) {\n                return SegmentedListTile(\n                  isFirst: true,\n                  isLast: true,\n                  leading: Icon(Symbols.my_location_rounded),\n                  title: Text('自動更新'.i18n),\n                  subtitle: Text('定期更新目前的所在地'.i18n),\n                  trailing: Switch(value: auto, onChanged: toggleAutoLocation),\n                );\n              },\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '自動定位功能將使用您的裝置上的 GPS，即使 DPIP 關閉或未在使用時，也會根據您的地理位置，自動更新您的所在地，提供即時的天氣和地震資訊，讓您隨時掌握當地最新狀況。'\n                .i18n,\n          ),\n        ),\n        if (locationAlwaysPermission != null)\n          Selector<SettingsLocationModel, bool>(\n            selector: (context, model) => model.auto,\n            builder: (context, auto, child) {\n              return Visibility(\n                visible: auto && !locationAlwaysPermission!.isGranted,\n                maintainAnimation: true,\n                maintainState: true,\n                child: AnimatedOpacity(\n                  opacity: auto && !locationAlwaysPermission!.isGranted ? 1 : 0,\n                  curve: const Interval(0.2, 1, curve: Easing.standard),\n                  duration: Durations.medium2,\n                  child: Padding(\n                    padding: const .all(16),\n                    child: Row(\n                      children: [\n                        Padding(\n                          padding: const .all(8),\n                          child: Icon(\n                            Symbols.warning_rounded,\n                            color: context.colors.error,\n                          ),\n                        ),\n                        const SizedBox(width: 8),\n                        Expanded(\n                          child: Text(\n                            '自動定位功能需要將位置權限提升至「$permissionType」以在背景使用。'.i18n,\n                            style: TextStyle(color: context.colors.error),\n                          ),\n                        ),\n                        TextButton(\n                          child: Text('設定'.i18n),\n                          onPressed: () => openAppSettings(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        if (notificationPermission != null)\n          Selector<SettingsLocationModel, bool>(\n            selector: (context, model) => model.auto,\n            builder: (context, auto, child) {\n              return Visibility(\n                visible: auto && !notificationPermission!.isGranted,\n                maintainAnimation: true,\n                maintainState: true,\n                child: AnimatedOpacity(\n                  opacity: auto && !notificationPermission!.isGranted ? 1 : 0,\n                  curve: const Interval(0.2, 1, curve: Easing.standard),\n                  duration: Durations.medium2,\n                  child: Padding(\n                    padding: const .all(16),\n                    child: Row(\n                      children: [\n                        Padding(\n                          padding: const .all(8),\n                          child: Icon(\n                            Symbols.warning_rounded,\n                            color: context.colors.error,\n                          ),\n                        ),\n                        const SizedBox(width: 8),\n                        Expanded(\n                          child: Text(\n                            '通知功能已被拒絕，請移至設定允許權限。'.i18n,\n                            style: TextStyle(color: context.colors.error),\n                          ),\n                        ),\n                        TextButton(\n                          child: Text('設定'.i18n),\n                          onPressed: () => openAppSettings(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        /* if (Platform.isAndroid)\n          Selector<SettingsLocationModel, bool>(\n            selector: (context, model) => model.auto,\n            builder: (context, auto, child) {\n              return Visibility(\n                visible: auto && !autoStartPermission,\n                maintainAnimation: true,\n                maintainState: true,\n                child: AnimatedOpacity(\n                  opacity: auto && !autoStartPermission ? 1 : 0,\n                  curve: const Interval(0.2, 1, curve: Easing.standard),\n                  duration: Durations.medium2,\n                  child: SettingsListTextSection(\n                    icon: Symbols.warning_rounded,\n                    iconColor: context.colors.error,\n                    content: '自啟動權限已被拒絕，請移至設定允許權限。'.i18n,\n                    contentColor: context.colors.error,\n                    trailing: TextButton(\n                      child: Text('設定'.i18n),\n                      onPressed: () => Autostarter.getAutoStartPermission(newTask: true),\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),*/\n        if (Platform.isAndroid)\n          Selector<SettingsLocationModel, bool>(\n            selector: (context, model) => model.auto,\n            builder: (context, auto, child) {\n              return Visibility(\n                visible: auto && !batteryOptimizationPermission,\n                maintainAnimation: true,\n                maintainState: true,\n                child: AnimatedOpacity(\n                  opacity: auto && !batteryOptimizationPermission ? 1 : 0,\n                  curve: const Interval(0.2, 1, curve: Easing.standard),\n                  duration: Durations.medium2,\n                  child: SectionText(\n                    leading: Icon(\n                      Symbols.warning_rounded,\n                      color: context.colors.error,\n                    ),\n                    child: Column(\n                      children: [\n                        Text(\n                          '省電策略已被拒絕，請移至設定允許權限。'.i18n,\n                          style: TextStyle(color: context.colors.error),\n                        ),\n                        TextButton(\n                          child: Text('設定'.i18n),\n                          onPressed: () =>\n                              DisableBatteryOptimization.showDisableBatteryOptimizationSettings(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        Consumer<SettingsLocationModel>(\n          builder: (context, model, child) {\n            String? loadingCode;\n\n            return StatefulBuilder(\n              builder: (context, setState) {\n                return SegmentedList(\n                  label: Text('所在地'.i18n),\n                  children: [\n                    ...model.favorited.mapIndexed((index, code) {\n                      final location = Global.location[code]!;\n                      final isCurrentLoading = loadingCode == code;\n                      final isSelected = code == model.code;\n\n                      return SegmentedListTile(\n                        isFirst: index == 0,\n                        leading: isCurrentLoading\n                            ? const LoadingIcon()\n                            : Icon(\n                                isSelected ? Symbols.check_rounded : null,\n                                color: context.colors.primary,\n                              ),\n                        title: Text(location.cityTownWithLevel),\n                        subtitle: Text(\n                          '$code・${location.lng.toStringAsFixed(2)}°E・${location.lat.toStringAsFixed(2)}°N',\n                        ),\n                        trailing: IconButton(\n                          icon: const Icon(Symbols.delete_rounded),\n                          color: context.colors.error,\n                          tooltip: '刪除',\n                          onPressed: isCurrentLoading ? null : () => model.unfavorite(code),\n                        ),\n                        enabled: !model.auto && loadingCode == null,\n                        onTap: isSelected\n                            ? null\n                            : () async {\n                                setState(() => loadingCode = code);\n                                try {\n                                  await ExpTech().updateDeviceLocation(\n                                    token: Preference.notifyToken,\n                                    coordinates: LatLng(\n                                      location.lat,\n                                      location.lng,\n                                    ),\n                                  );\n\n                                  if (!context.mounted) return;\n                                  model.setCode(code);\n                                } catch (e, s) {\n                                  if (!context.mounted) return;\n                                  TalkerManager.instance.error(\n                                    'Failed to set location code',\n                                    e,\n                                    s,\n                                  );\n                                  showToast(\n                                    context,\n                                    ToastWidget.text(\n                                      '設定所在地時發生錯誤，請稍候再試一次。'.i18n,\n                                    ),\n                                  );\n                                }\n                                setState(() => loadingCode = null);\n                              },\n                      );\n                    }),\n                    SegmentedListTile(\n                      isFirst: model.favorited.isEmpty,\n                      isLast: true,\n                      leading: Icon(Symbols.add_circle_rounded),\n                      title: Text('新增地點'.i18n),\n                      enabled: loadingCode == null,\n                      onTap: () => SettingsLocationSelectRoute().push(context),\n                    ),\n                  ],\n                );\n              },\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/location/select/[city]/page.dart",
    "content": "/// City-level district selection page for location settings.\nlibrary;\n\nimport 'dart:collection';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/toast.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A page listing all districts within a given [city] for location selection.\n///\n/// Tap a district to save it as the user's location and navigate back to the\n/// location settings page:\n///\n/// ```dart\n/// SettingsLocationSelectCityPage(city: '台北市')\n/// ```\nclass SettingsLocationSelectCityPage extends StatefulWidget {\n  /// The city name used to filter available districts.\n  final String city;\n\n  /// Creates a [SettingsLocationSelectCityPage] for the given [city].\n  const SettingsLocationSelectCityPage({super.key, required this.city});\n\n  @override\n  State<SettingsLocationSelectCityPage> createState() => _SettingsLocationSelectCityPageState();\n}\n\nclass _SettingsLocationSelectCityPageState extends State<SettingsLocationSelectCityPage> {\n  String? _loadingCode;\n\n  @override\n  Widget build(BuildContext context) {\n    final towns = Global.location.entries\n        .where((e) => e.value.cityWithLevel == widget.city)\n        .toList();\n\n    final length = towns.length;\n\n    return CustomScrollView(\n      slivers: [\n        SliverSegmentedList(\n          label: Text(widget.city),\n          children: [\n            for (final (index, MapEntry(key: code, value: town)) in towns.indexed)\n              Selector<SettingsLocationModel, bool>(\n                selector: (context, model) => model.isFavorited(code),\n                builder: (context, isFavorited, child) {\n                  final isLoading = _loadingCode == code;\n\n                  return SegmentedListTile(\n                    isFirst: index == 0,\n                    isLast: index == length - 1,\n                    title: Text(town.cityTownWithLevel),\n                    subtitle: Text(\n                      '$code・${town.lng.toStringAsFixed(2)}°E・${town.lat.toStringAsFixed(2)}°N',\n                    ),\n                    trailing: isLoading\n                        ? const LoadingIcon()\n                        : isFavorited\n                        ? const Icon(Symbols.star_rounded, fill: 1)\n                        : null,\n                    enabled: _loadingCode == null,\n                    onTap: isFavorited\n                        ? null\n                        : () async {\n                            setState(() => _loadingCode = code);\n\n                            try {\n                              context.location.favorite(code);\n\n                              await ExpTech().updateDeviceLocation(\n                                token: Preference.notifyToken,\n                                coordinates: LatLng(town.lat, town.lng),\n                              );\n                              if (!context.mounted) return;\n\n                              context.location.setCode(code);\n                              if (!context.mounted) return;\n\n                              context.popUntil(\n                                SettingsLocationRoute().location,\n                              );\n                            } catch (e, s) {\n                              if (!context.mounted) return;\n                              TalkerManager.instance.error(\n                                'Failed to set location',\n                                e,\n                                s,\n                              );\n\n                              setState(() => _loadingCode = null);\n\n                              showToast(\n                                context,\n                                ToastWidget.text(\n                                  '設定所在地時發生錯誤，請稍候再試一次。'.i18n,\n                                ),\n                              );\n                            }\n                          },\n                  );\n                },\n              ),\n          ],\n        ),\n        SliverPadding(padding: .only(bottom: context.padding.bottom + 16)),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/location/select/page.dart",
    "content": "/// Location selection page listing all available cities.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A page that lists all cities for location selection.\n///\n/// Tap a city to navigate to the district selection page for that city.\n/// The currently active location city is highlighted with a subtitle.\nclass SettingsLocationSelectPage extends StatelessWidget {\n  /// Creates a [SettingsLocationSelectPage].\n  const SettingsLocationSelectPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final entries = Global.location.entries;\n\n    final Set<String> walked = {};\n    final locations = entries\n        .where((e) => walked.add(e.value.cityWithLevel))\n        .map((e) => e.value)\n        .toList();\n\n    final length = locations.length;\n    print('length = ${locations.length}');\n    print(locations);\n\n    final code = context.useLocation.code;\n\n    return CustomScrollView(\n      slivers: [\n        SliverSegmentedList(\n          label: Text('縣市'.i18n),\n          children: [\n            for (final (index, location) in locations.indexed)\n              SegmentedListTile(\n                isFirst: index == 0,\n                isLast: index == length - 1,\n                title: Text(location.cityWithLevel),\n                subtitle: code != null && location.cityWithLevel == code.getLocation().cityWithLevel\n                    ? Text('目前所在地'.i18n)\n                    : null,\n                trailing: const Icon(Symbols.chevron_right_rounded),\n                onTap: () => SettingsLocationSelectCityRoute(\n                  city: location.cityWithLevel,\n                ).push(context),\n              ),\n          ],\n        ),\n        SliverPadding(padding: .only(bottom: context.padding.bottom + 16)),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/map/page.dart",
    "content": "/// Map display settings page.\nlibrary;\n\nimport 'package:dpip/app/map/_widgets/layer_toggle_sheet.dart';\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/map.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/widgets/layout.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/typography.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A settings page for adjusting map display options such as initial layers,\n/// auto-zoom, and animation frame rate.\n///\n/// Requires [SettingsMapModel] in the widget tree.\nclass SettingsMapPage extends StatelessWidget {\n  /// Creates a [SettingsMapPage].\n  const SettingsMapPage({super.key});\n\n  /// Opens the layer toggle bottom sheet.\n  Future<void> showLayerSheet(BuildContext context) {\n    return showModalBottomSheet(\n      context: context,\n      useRootNavigator: true,\n      useSafeArea: true,\n      isScrollControlled: true,\n      constraints: context.bottomSheetConstraints,\n      builder: (context) {\n        return LayerToggleSheet(\n          activeLayers: context.map.layers,\n          currentBaseMap: context.map.baseMap,\n          onLayerChanged: (layer, show, activeLayers) {\n            context.map.setLayers(activeLayers);\n          },\n          onBaseMapChanged: (baseMap) {\n            context.map.setBaseMapType(baseMap);\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) => ListView(\n    padding: .only(\n      top: 16,\n      bottom: 16 + context.padding.bottom,\n    ),\n    children: [\n      SettingsHeader(\n        icon: Symbols.map_rounded,\n        title: Text('地圖'.i18n),\n        subtitle: Text('調整地圖的顯示樣式'.i18n),\n      ),\n      const SizedBox(height: 16),\n      SegmentedList(\n        children: [\n          SegmentedListTile(\n            isFirst: true,\n            leading: ContainedIcon(\n              Symbols.layers_rounded,\n              color: Colors.tealAccent,\n            ),\n            title: Text('初始圖層'.i18n),\n            subtitle: Text('調整地圖的底圖以及初始顯示的圖層'.i18n),\n            trailing: Icon(Symbols.chevron_right_rounded),\n            onTap: () => showLayerSheet(context),\n          ),\n          Selector<SettingsMapModel, bool>(\n            selector: (_, model) => model.autoZoom,\n            builder: (context, autoZoom, child) {\n              return SegmentedListTile(\n                leading: ContainedIcon(\n                  Symbols.zoom_in_rounded,\n                  color: Colors.blueAccent,\n                ),\n                title: Text('自動縮放'.i18n),\n                subtitle: Text('接收到檢知時自動縮放地圖'.i18n),\n                trailing: Switch(\n                  value: autoZoom,\n                  onChanged: (value) => context.map.setAutoZoom(value),\n                ),\n              );\n            },\n          ),\n          Selector<SettingsMapModel, int>(\n            selector: (_, model) => model.updateInterval,\n            builder: (context, updateInterval, child) {\n              final maxFpsAllowed = WidgetsBinding\n                  .instance\n                  .platformDispatcher\n                  .views\n                  .first\n                  .display\n                  .refreshRate\n                  .floorToDouble();\n\n              return SegmentedListTile(\n                isLast: true,\n                leading: ContainedIcon(\n                  Symbols.animation_rounded,\n                  color: Colors.orangeAccent,\n                ),\n                title: Text('動畫幀率'.i18n),\n                subtitle: Text('調整強震監視器震波模擬動畫的流暢度'.i18n),\n                content: Column(\n                  crossAxisAlignment: .start,\n                  children: [\n                    Row(\n                      children: [\n                        LabelText.small('1'),\n                        Expanded(\n                          child: Slider(\n                            value: updateInterval.toDouble().clamp(\n                              1,\n                              maxFpsAllowed,\n                            ),\n                            min: 1,\n                            max: maxFpsAllowed,\n                            divisions: maxFpsAllowed.floor() ~/ 5,\n                            label: '$updateInterval FPS',\n                            onChanged: (value) => context.map.setUpdateInterval(value.floor()),\n                          ),\n                        ),\n                        LabelText.small('${maxFpsAllowed.floor()}'),\n                      ],\n                    ),\n                    if (updateInterval > 20)\n                      Layout.row.left[8](\n                        children: [\n                          Icon(\n                            Symbols.warning_rounded,\n                            color: context.theme.extendedColors.amber,\n                            size: 16,\n                          ),\n                          Expanded(\n                            child: BodyText.small(\n                              '過高的動畫幀率可能會造成卡頓或裝置發熱'.i18n,\n                              color: context.theme.extendedColors.amber,\n                            ),\n                          ),\n                        ],\n                      ),\n                  ],\n                ),\n              );\n            },\n          ),\n        ],\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(1.eew)/eew/page.dart",
    "content": "/// EEW notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/eew_notify_section.dart';\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring EEW (Earthquake Early Warning) notification settings\n/// and testing EEW notification sounds.\nclass SettingsNotifyEewPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyEewPage].\n  const SettingsNotifyEewPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, EewNotifyType>(\n          selector: (context, model) => model.eew,\n          builder: (context, value, child) {\n            return EewNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setEew(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '緊急地震速報(重大)'.i18n,\n              subtitle: Text('最大震度 5 弱以上 且\\n所在地(鄉鎮)預估震度 4 以上'.i18n),\n              type: 'eew_alert-important-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '緊急地震速報(一般)'.i18n,\n              subtitle: Text('最大震度 5 弱以上 且\\n所在地(鄉鎮)預估震度 2 以上'.i18n),\n              type: 'eew_alert-general-v2',\n            ),\n            SoundListTile(\n              title: '緊急地震速報(無聲)'.i18n,\n              subtitle: Text('最大震度 5 弱以上 且\\n所在地(鄉鎮)預估震度 1 以上'.i18n),\n              type: 'eew_alert-silent-v2',\n            ),\n            SoundListTile(\n              title: '地震速報(重大)'.i18n,\n              subtitle: Text('所在地(鄉鎮)預估震度 4 以上'.i18n),\n              type: 'eew-important-v2',\n            ),\n            SoundListTile(\n              title: '地震速報(一般)'.i18n,\n              subtitle: Text('所在地(鄉鎮)預估震度 2 以上'.i18n),\n              type: 'eew-general-v2',\n            ),\n            SoundListTile(\n              title: '地震速報(無聲)'.i18n,\n              subtitle: Text('所在地(鄉鎮)預估震度 1 以上'.i18n),\n              type: 'eew-silence-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(2.earthquake)/intensity/page.dart",
    "content": "/// Intensity report notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/earthquake_notify_section.dart';\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring intensity report notification settings and testing\n/// intensity report sounds.\nclass SettingsNotifyIntensityPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyIntensityPage].\n  const SettingsNotifyIntensityPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n          selector: (context, model) => model.intensity,\n          builder: (context, value, child) {\n            return EarthquakeNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setIntensity(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '震度速報(一般)'.i18n,\n              subtitle: Text('所在地(鄉鎮)實測震度 3 以上'.i18n),\n              type: 'int_report-general-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '震度速報(無聲通知)'.i18n,\n              subtitle: Text('所在地(鄉鎮)實測震度 1 以上'.i18n),\n              type: 'int_report-silence-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(2.earthquake)/monitor/page.dart",
    "content": "/// Strong motion monitor notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/earthquake_notify_section.dart';\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring strong motion monitor notification settings and\n/// testing monitor notification sounds.\nclass SettingsNotifyMonitorPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyMonitorPage].\n  const SettingsNotifyMonitorPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n          selector: (context, model) => model.monitor,\n          builder: (context, value, child) {\n            return EarthquakeNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setMonitor(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '強震監視器(一般)'.i18n,\n              subtitle: Text('偵測到晃動'.i18n),\n              type: 'eq-v2',\n              isFirst: true,\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(2.earthquake)/report/page.dart",
    "content": "/// Earthquake report notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/earthquake_notify_section.dart';\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring earthquake report notification settings and testing\n/// report sounds.\nclass SettingsNotifyReportPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyReportPage].\n  const SettingsNotifyReportPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n          selector: (context, model) => model.report,\n          builder: (context, value, child) {\n            return EarthquakeNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setReport(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '地震報告(一般)'.i18n,\n              subtitle: Text('所在地(縣市)實測震度 3 以上'.i18n),\n              type: 'report-general-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '地震報告(無聲通知)'.i18n,\n              subtitle: Text('所在地(縣市)實測震度 1 以上'.i18n),\n              type: 'report-silence-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(3.weather)/advisory/page.dart",
    "content": "/// Weather advisory notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/app/settings/notify/_widgets/weather_notify_section.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring weather advisory notification settings and testing\n/// advisory sounds.\nclass SettingsNotifyAdvisoryPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyAdvisoryPage].\n  const SettingsNotifyAdvisoryPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, WeatherNotifyType>(\n          selector: (context, model) => model.weatherAdvisory,\n          builder: (context, value, child) {\n            return WeatherNotifySection(\n              value: value,\n              onChanged: (value) =>\n                  context.read<SettingsNotificationModel>().setWeatherAdvisory(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '重大'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布紅色燈號之\\n天氣警特報'.i18n),\n              type: 'weather_major-important-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '一般'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布上述除外燈號之\\n天氣警特報'.i18n),\n              type: 'weather_minor-general-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(3.weather)/evacuation/page.dart",
    "content": "/// Evacuation / disaster prevention notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/app/settings/notify/_widgets/weather_notify_section.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring evacuation / disaster information notification\n/// settings and testing the associated sounds.\nclass SettingsNotifyEvacuationPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyEvacuationPage].\n  const SettingsNotifyEvacuationPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, WeatherNotifyType>(\n          selector: (context, model) => model.evacuation,\n          builder: (context, value, child) {\n            return WeatherNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setEvacuation(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '重大'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布防災警訊時'.i18n),\n              type: 'evacuation_major-important-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '一般'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布防災資訊時'.i18n),\n              type: 'evacuation_minor-general-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(3.weather)/thunderstorm/page.dart",
    "content": "/// Thunderstorm notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/app/settings/notify/_widgets/weather_notify_section.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring thunderstorm instant message notification settings\n/// and testing the associated sounds.\nclass SettingsNotifyThunderstormPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyThunderstormPage].\n  const SettingsNotifyThunderstormPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, WeatherNotifyType>(\n          selector: (context, model) => model.thunderstorm,\n          builder: (context, value, child) {\n            return WeatherNotifySection(\n              value: value,\n              onChanged: (value) =>\n                  context.read<SettingsNotificationModel>().setThunderstorm(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '重大'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布山區暴雨時'.i18n),\n              type: 'thunderstorm-important-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '一般'.i18n,\n              subtitle: Text('所在地(鄉鎮)發布雷雨即時訊息時'.i18n),\n              type: 'thunderstorm-general-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(4.tsunami)/tsunami/page.dart",
    "content": "/// Tsunami notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/app/settings/notify/_widgets/tsunami_notify_section.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring tsunami notification settings and testing tsunami\n/// notification sounds.\nclass SettingsNotifyTsunamiPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyTsunamiPage].\n  const SettingsNotifyTsunamiPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, TsunamiNotifyType>(\n          selector: (context, model) => model.tsunami,\n          builder: (context, value, child) {\n            return TsunamiNotifySection(\n              value: value,\n              onChanged: (value) => context.read<SettingsNotificationModel>().setTsunami(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '重大'.i18n,\n              subtitle: Text('海嘯警報發布時'.i18n),\n              type: 'tsunami-important-v2',\n              isFirst: true,\n            ),\n            SoundListTile(\n              title: '一般'.i18n,\n              subtitle: Text('海嘯消息發布時'.i18n),\n              type: 'tsunami-general-v2',\n            ),\n            SoundListTile(\n              title: '太平洋海嘯消息(無聲通知)'.i18n,\n              subtitle: Text('太平洋海嘯消息發布時'.i18n),\n              type: 'tsunami-silent-v2',\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/(5.basic)/announcement/page.dart",
    "content": "/// Announcement notification settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_widgets/basic_notify_section.dart';\nimport 'package:dpip/app/settings/notify/_widgets/sound_list_tile.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for configuring announcement notification settings and testing the\n/// announcement sound.\nclass SettingsNotifyAnnouncementPage extends StatelessWidget {\n  /// Creates a [SettingsNotifyAnnouncementPage].\n  const SettingsNotifyAnnouncementPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Selector<SettingsNotificationModel, BasicNotifyType>(\n          selector: (context, model) => model.announcement,\n          builder: (context, value, child) {\n            return BasicNotifySection(\n              value: value,\n              onChanged: (value) =>\n                  context.read<SettingsNotificationModel>().setAnnouncement(value),\n            );\n          },\n        ),\n        SegmentedList(\n          label: Text('音效測試'.i18n),\n          children: [\n            SoundListTile(\n              title: '公告'.i18n,\n              subtitle: Text('發送公告時'.i18n),\n              type: 'announcement-general-v2',\n              isFirst: true,\n              isLast: true,\n            ),\n          ],\n        ),\n        SectionText(\n          child: Text(\n            '音效測試為在裝置上執行的本地通知，僅用於確認裝置在接收通知時是否能正常播放音效。此測試不會向伺服器發送任何請求'.i18n,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_lib/utils.dart",
    "content": "/// Utility helpers for notification settings pages.\n///\n/// Provides shared toast display functions and generic setters for each\n/// notification type that show success or error feedback after the async\n/// operation completes.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/utils/toast.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A checkmark icon used as the selected-state trailing widget.\nconst check = Icon(Symbols.check_rounded);\n\n/// A loading spinner used as the in-progress trailing widget.\nconst loading = LoadingIcon();\n\n/// An empty icon used as the unselected trailing widget.\nconst empty = Icon(null);\n\n/// Shows a success toast confirming the notification setting was updated.\nvoid showSuccessToast(BuildContext context) {\n  showToast(\n    context,\n    ToastWidget.text(\n      '已更新通知設定'.i18n,\n      icon: const Icon(Symbols.check_rounded),\n    ),\n  );\n}\n\n/// Shows an error toast indicating the notification setting update failed.\nvoid showErrorToast(BuildContext context) {\n  showToast(\n    context,\n    ToastWidget.text(\n      '更新通知設定失敗'.i18n,\n      icon: const Icon(Symbols.error_rounded),\n    ),\n  );\n}\n\n/// Calls [setter] with [value], then shows a success or error toast.\nFuture setEewNotifyType(\n  BuildContext context,\n  EewNotifyType value,\n  Future Function(EewNotifyType value) setter,\n) async {\n  try {\n    await setter(value);\n\n    if (!context.mounted) return;\n    showSuccessToast(context);\n  } catch (e) {\n    if (!context.mounted) return;\n    showErrorToast(context);\n  }\n}\n\n/// Calls [setter] with [value], then shows a success or error toast.\nFuture setEarthquakeNotifyType(\n  BuildContext context,\n  EarthquakeNotifyType value,\n  Future Function(EarthquakeNotifyType value) setter,\n) async {\n  try {\n    await setter(value);\n\n    if (!context.mounted) return;\n    showSuccessToast(context);\n  } catch (e) {\n    if (!context.mounted) return;\n    showErrorToast(context);\n  }\n}\n\n/// Calls [setter] with [value], then shows a success or error toast.\nFuture setWeatherNotifyType(\n  BuildContext context,\n  WeatherNotifyType value,\n  Future Function(WeatherNotifyType value) setter,\n) async {\n  try {\n    await setter(value);\n\n    if (!context.mounted) return;\n    showSuccessToast(context);\n  } catch (e) {\n    if (!context.mounted) return;\n    showErrorToast(context);\n  }\n}\n\n/// Calls [setter] with [value], then shows a success or error toast.\nFuture setTsunamiNotifyType(\n  BuildContext context,\n  TsunamiNotifyType value,\n  Future Function(TsunamiNotifyType value) setter,\n) async {\n  try {\n    await setter(value);\n\n    if (!context.mounted) return;\n    showSuccessToast(context);\n  } catch (e) {\n    if (!context.mounted) return;\n    showErrorToast(context);\n  }\n}\n\n/// Calls [setter] with [value], then shows a success or error toast.\nFuture setBasicNotifyType(\n  BuildContext context,\n  BasicNotifyType value,\n  Future Function(BasicNotifyType value) setter,\n) async {\n  try {\n    await setter(value);\n\n    if (!context.mounted) return;\n    showSuccessToast(context);\n  } catch (e) {\n    if (!context.mounted) return;\n    showErrorToast(context);\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/basic_notify_section.dart",
    "content": "/// Basic notification type selection section widget.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A list section for selecting the [BasicNotifyType].\n///\n/// Displays each available type as a selectable tile with a loading indicator\n/// while the change is being saved. Pass the current [value] and an\n/// [onChanged] callback that persists the new type:\n///\n/// ```dart\n/// BasicNotifySection(\n///   value: BasicNotifyType.all,\n///   onChanged: (v) => model.setAnnouncement(v),\n/// )\n/// ```\nclass BasicNotifySection extends StatefulWidget {\n  /// The currently active notification type.\n  final BasicNotifyType value;\n\n  /// Called with the new type when the user selects a different option.\n  final Future Function(BasicNotifyType value) onChanged;\n\n  /// Creates a [BasicNotifySection].\n  const BasicNotifySection({\n    super.key,\n    required this.value,\n    required this.onChanged,\n  });\n\n  @override\n  State<BasicNotifySection> createState() => _BasicNotifySectionState();\n}\n\nclass _BasicNotifySectionState extends State<BasicNotifySection> {\n  BasicNotifyType? _loading;\n\n  Future onChanged(BasicNotifyType value) async {\n    setState(() => _loading = value);\n    await setBasicNotifyType(context, value, widget.onChanged);\n    setState(() => _loading = null);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final values = {\n      BasicNotifyType.all: (\n        title: '接收全部'.i18n,\n        icon: Symbols.notifications_rounded,\n      ),\n      BasicNotifyType.off: (\n        title: '關閉'.i18n,\n        icon: Symbols.notifications_off_rounded,\n      ),\n    };\n\n    final entry = values.entries.toList();\n\n    return SegmentedList(\n      label: Text('接收類別'.i18n),\n      children: [\n        for (int i = 0; i < entry.length; i++)\n          SegmentedListTile(\n            leading: Icon(entry[i].value.icon),\n            title: Text(entry[i].value.title),\n            trailing: _loading == entry[i].key\n                ? loading\n                : (widget.value == entry[i].key)\n                ? check\n                : empty,\n            enabled: _loading == null,\n            onTap: _loading == null ? () => onChanged(entry[i].key) : null,\n            isFirst: i == 0,\n            isLast: i == entry.length - 1,\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/earthquake_notify_section.dart",
    "content": "/// Earthquake notification type selection section widget.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A list section for selecting the [EarthquakeNotifyType].\n///\n/// Shows each available type as a selectable tile with a loading indicator\n/// while the change is being saved. Pass the current [value] and an\n/// [onChanged] callback:\n///\n/// ```dart\n/// EarthquakeNotifySection(\n///   value: EarthquakeNotifyType.all,\n///   onChanged: (v) => model.setMonitor(v),\n/// )\n/// ```\nclass EarthquakeNotifySection extends StatefulWidget {\n  /// The currently active notification type.\n  final EarthquakeNotifyType value;\n\n  /// Called with the new type when the user selects a different option.\n  final Future Function(EarthquakeNotifyType value) onChanged;\n\n  /// Creates an [EarthquakeNotifySection].\n  const EarthquakeNotifySection({\n    super.key,\n    required this.value,\n    required this.onChanged,\n  });\n\n  @override\n  State<EarthquakeNotifySection> createState() => _EarthquakeNotifySectionState();\n}\n\nclass _EarthquakeNotifySectionState extends State<EarthquakeNotifySection> {\n  EarthquakeNotifyType? _loading;\n\n  Future onChanged(EarthquakeNotifyType value) async {\n    setState(() => _loading = value);\n    await setEarthquakeNotifyType(context, value, widget.onChanged);\n    setState(() => _loading = null);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final values = {\n      EarthquakeNotifyType.all: (\n        title: '接收全部'.i18n,\n        icon: Symbols.notification_add_rounded,\n      ),\n      EarthquakeNotifyType.localIntensityAbove1: (\n        title: '所在地震度1以上'.i18n,\n        icon: Symbols.notifications_rounded,\n      ),\n      EarthquakeNotifyType.off: (\n        title: '關閉'.i18n,\n        icon: Symbols.notifications_off_rounded,\n      ),\n    };\n\n    final entry = values.entries.toList();\n\n    return SegmentedList(\n      label: Text('接收類別'.i18n),\n      children: [\n        for (int i = 0; i < entry.length; i++)\n          SegmentedListTile(\n            leading: Icon(entry[i].value.icon),\n            title: Text(entry[i].value.title),\n            trailing: _loading == entry[i].key\n                ? loading\n                : (widget.value == entry[i].key)\n                ? check\n                : empty,\n            enabled: _loading == null,\n            onTap: _loading == null ? () => onChanged(entry[i].key) : null,\n            isFirst: i == 0,\n            isLast: i == entry.length - 1,\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/eew_notify_section.dart",
    "content": "/// EEW notification type selection section widget.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A list section for selecting the [EewNotifyType].\n///\n/// Shows each available EEW threshold as a selectable tile with a loading\n/// indicator while the change is being saved. Pass the current [value] and an\n/// [onChanged] callback:\n///\n/// ```dart\n/// EewNotifySection(\n///   value: EewNotifyType.all,\n///   onChanged: (v) => model.setEew(v),\n/// )\n/// ```\nclass EewNotifySection extends StatefulWidget {\n  /// The currently active EEW notification type.\n  final EewNotifyType value;\n\n  /// Called with the new type when the user selects a different option.\n  final Future Function(EewNotifyType value) onChanged;\n\n  /// Creates an [EewNotifySection].\n  const EewNotifySection({\n    super.key,\n    required this.value,\n    required this.onChanged,\n  });\n\n  @override\n  State<EewNotifySection> createState() => _EewNotifySectionState();\n}\n\nclass _EewNotifySectionState extends State<EewNotifySection> {\n  EewNotifyType? _loading;\n\n  Future onChanged(EewNotifyType value) async {\n    setState(() => _loading = value);\n    await setEewNotifyType(context, value, widget.onChanged);\n    setState(() => _loading = null);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final values = {\n      EewNotifyType.all: (\n        title: '接收全部'.i18n,\n        icon: Symbols.notification_add_rounded,\n      ),\n      EewNotifyType.localIntensityAbove1: (\n        title: '所在地震度1以上'.i18n,\n        icon: Symbols.notifications_rounded,\n      ),\n      EewNotifyType.localIntensityAbove4: (\n        title: '所在地震度4以上'.i18n,\n        icon: Symbols.notification_important_rounded,\n      ),\n    };\n\n    final entry = values.entries.toList();\n\n    return SegmentedList(\n      label: Text('接收類別'.i18n),\n      children: [\n        for (int i = 0; i < entry.length; i++)\n          SegmentedListTile(\n            leading: Icon(entry[i].value.icon),\n            title: Text(entry[i].value.title),\n            trailing: _loading == entry[i].key\n                ? loading\n                : (widget.value == entry[i].key)\n                ? check\n                : empty,\n            enabled: _loading == null,\n            onTap: _loading == null ? () => onChanged(entry[i].key) : null,\n            isFirst: i == 0,\n            isLast: i == entry.length - 1,\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/sound_list_tile.dart",
    "content": "/// Sound test list tile widget for notification settings pages.\nlibrary;\n\nimport 'dart:async';\nimport 'dart:io';\n\nimport 'package:awesome_notifications/awesome_notifications.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/loading_icon.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\n/// A list tile that plays a test notification when tapped.\n///\n/// Disables itself for 2 seconds after tapping to prevent rapid repeated\n/// triggers. Use [type] to specify the notification channel key:\n///\n/// ```dart\n/// SoundListTile(\n///   title: '緊急地震速報(重大)',\n///   type: 'eew_alert-important-v2',\n///   isFirst: true,\n/// )\n/// ```\nclass SoundListTile extends StatefulWidget {\n  /// The display label for this sound test row.\n  final String title;\n\n  /// An optional description shown below [title].\n  final Widget? subtitle;\n\n  /// The notification channel key used to trigger the test notification.\n  final String type;\n\n  /// Whether this tile is the first in its containing list.\n  final bool isFirst;\n\n  /// Whether this tile is the last in its containing list.\n  final bool isLast;\n\n  /// Creates a [SoundListTile].\n  const SoundListTile({\n    super.key,\n    required this.title,\n    required this.type,\n    this.subtitle,\n    this.isFirst = false,\n    this.isLast = false,\n  });\n\n  @override\n  State<SoundListTile> createState() => _SoundListTileState();\n}\n\nclass _SoundListTileState extends State<SoundListTile> {\n  bool _enabled = true;\n\n  void onTap() {\n    setState(() => _enabled = false);\n\n    final content = Global.notifyTestContent[widget.type]!;\n\n    AwesomeNotifications().createNotification(\n      content: NotificationContent(\n        id: -1,\n        channelKey: widget.type,\n        title: '[測試] ${content.title}',\n        body: '＊＊＊這是測試訊息＊＊＊${(Platform.isIOS) ? \"\\n\" : \"<br>\"} ${content.body}',\n        notificationLayout: NotificationLayout.BigText,\n      ),\n    );\n\n    Timer(const Duration(seconds: 2), () {\n      if (!mounted) return;\n      setState(() => _enabled = true);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SegmentedListTile(\n      title: Text(widget.title),\n      subtitle: widget.subtitle,\n      trailing: _enabled ? const Icon(Symbols.play_circle_rounded) : const LoadingIcon(),\n      enabled: _enabled,\n      isFirst: widget.isFirst,\n      isLast: widget.isLast,\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/tsunami_notify_section.dart",
    "content": "/// Tsunami notification type selection section widget.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A list section for selecting the [TsunamiNotifyType].\n///\n/// Shows each threshold as a selectable tile with a loading indicator while\n/// the change is being saved. Pass the current [value] and an [onChanged]\n/// callback:\n///\n/// ```dart\n/// TsunamiNotifySection(\n///   value: TsunamiNotifyType.all,\n///   onChanged: (v) => model.setTsunami(v),\n/// )\n/// ```\nclass TsunamiNotifySection extends StatefulWidget {\n  /// The currently active tsunami notification type.\n  final TsunamiNotifyType value;\n\n  /// Called with the new type when the user selects a different option.\n  final Future Function(TsunamiNotifyType value) onChanged;\n\n  /// Creates a [TsunamiNotifySection].\n  const TsunamiNotifySection({\n    super.key,\n    required this.value,\n    required this.onChanged,\n  });\n\n  @override\n  State<TsunamiNotifySection> createState() => _TsunamiNotifySectionState();\n}\n\nclass _TsunamiNotifySectionState extends State<TsunamiNotifySection> {\n  TsunamiNotifyType? _loading;\n\n  Future onChanged(TsunamiNotifyType value) async {\n    setState(() => _loading = value);\n    await setTsunamiNotifyType(context, value, widget.onChanged);\n    setState(() => _loading = null);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final values = {\n      TsunamiNotifyType.all: (\n        title: '海嘯消息、海嘯警報'.i18n,\n        icon: Symbols.notifications_rounded,\n      ),\n      TsunamiNotifyType.warningOnly: (\n        title: '只接收海嘯警報'.i18n,\n        icon: Symbols.notification_important_rounded,\n      ),\n    };\n\n    final entry = values.entries.toList();\n\n    return SegmentedList(\n      label: Text('接收類別'.i18n),\n      children: [\n        for (int i = 0; i < entry.length; i++)\n          SegmentedListTile(\n            leading: Icon(entry[i].value.icon),\n            title: Text(entry[i].value.title),\n            trailing: _loading == entry[i].key\n                ? loading\n                : (widget.value == entry[i].key)\n                ? check\n                : empty,\n            enabled: _loading == null,\n            onTap: _loading == null ? () => onChanged(entry[i].key) : null,\n            isFirst: i == 0,\n            isLast: i == entry.length - 1,\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/_widgets/weather_notify_section.dart",
    "content": "/// Weather notification type selection section widget.\nlibrary;\n\nimport 'package:dpip/app/settings/notify/_lib/utils.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A list section for selecting the [WeatherNotifyType].\n///\n/// Shows each option as a selectable tile with a loading indicator while the\n/// change is being saved. Pass the current [value] and an [onChanged]\n/// callback:\n///\n/// ```dart\n/// WeatherNotifySection(\n///   value: WeatherNotifyType.local,\n///   onChanged: (v) => model.setThunderstorm(v),\n/// )\n/// ```\nclass WeatherNotifySection extends StatefulWidget {\n  /// The currently active weather notification type.\n  final WeatherNotifyType value;\n\n  /// Called with the new type when the user selects a different option.\n  final Future Function(WeatherNotifyType value) onChanged;\n\n  /// Creates a [WeatherNotifySection].\n  const WeatherNotifySection({\n    super.key,\n    required this.value,\n    required this.onChanged,\n  });\n\n  @override\n  State<WeatherNotifySection> createState() => _WeatherNotifySectionState();\n}\n\nclass _WeatherNotifySectionState extends State<WeatherNotifySection> {\n  WeatherNotifyType? _loading;\n\n  Future onChanged(WeatherNotifyType value) async {\n    setState(() => _loading = value);\n    await setWeatherNotifyType(context, value, widget.onChanged);\n    setState(() => _loading = null);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final values = {\n      WeatherNotifyType.local: (\n        title: '接收所在地'.i18n,\n        icon: Symbols.notifications_rounded,\n      ),\n      WeatherNotifyType.off: (\n        title: '關閉'.i18n,\n        icon: Symbols.notifications_off_rounded,\n      ),\n    };\n\n    final entry = values.entries.toList();\n\n    return SegmentedList(\n      label: Text('接收類別'.i18n),\n      children: [\n        for (int i = 0; i < entry.length; i++)\n          SegmentedListTile(\n            leading: Icon(entry[i].value.icon),\n            title: Text(entry[i].value.title),\n            trailing: _loading == entry[i].key\n                ? loading\n                : (widget.value == entry[i].key)\n                ? check\n                : empty,\n            enabled: _loading == null,\n            onTap: _loading == null ? () => onChanged(entry[i].key) : null,\n            isFirst: i == 0,\n            isLast: i == entry.length - 1,\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/notify/page.dart",
    "content": "/// Notification settings index page.\nlibrary;\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\n\n/// A page showing all notification category settings.\n///\n/// Fetches the current notification preferences from the server if they have\n/// not been loaded yet. Requires [SettingsNotificationModel] and\n/// [SettingsLocationModel] in the widget tree.\nclass SettingsNotifyPage extends StatefulWidget {\n  /// Creates a [SettingsNotifyPage].\n  const SettingsNotifyPage({super.key});\n\n  @override\n  State<SettingsNotifyPage> createState() => _SettingsNotifyPageState();\n}\n\nclass _SettingsNotifyPageState extends State<SettingsNotifyPage> {\n  bool isLoading = false;\n\n  bool get hasLocation =>\n      GlobalProviders.location.coordinates != null ||\n      GlobalProviders.location.code != null ||\n      (Preference.locationAuto ?? false);\n\n  String getBasicNotifyTypeName(BasicNotifyType value) => switch (value) {\n    BasicNotifyType.off => '關閉'.i18n,\n    BasicNotifyType.all => '接收全部'.i18n,\n  };\n\n  String getEarthquakeNotifyTypeName(EarthquakeNotifyType value) => switch (value) {\n    EarthquakeNotifyType.off => '關閉'.i18n,\n    EarthquakeNotifyType.localIntensityAbove1 => '所在地震度1以上'.i18n,\n    EarthquakeNotifyType.all => '接收全部'.i18n,\n  };\n\n  String getEewNotifyTypeName(EewNotifyType value) => switch (value) {\n    EewNotifyType.localIntensityAbove4 => '所在地震度4以上'.i18n,\n    EewNotifyType.localIntensityAbove1 => '所在地震度1以上'.i18n,\n    EewNotifyType.all => '接收全部'.i18n,\n  };\n\n  String getTsunamiNotifyTypeName(TsunamiNotifyType value) => switch (value) {\n    TsunamiNotifyType.warningOnly => '只接收海嘯警報'.i18n,\n    TsunamiNotifyType.all => '海嘯消息、海嘯警報'.i18n,\n  };\n\n  String getWeatherNotifyTypeName(WeatherNotifyType value) => switch (value) {\n    WeatherNotifyType.off => '關閉'.i18n,\n    WeatherNotifyType.local => '接收所在地'.i18n,\n  };\n\n  Widget _buildIconContainer({\n    required IconData icon,\n    required Color color,\n  }) {\n    return Container(\n      padding: const .all(8),\n      decoration: BoxDecoration(\n        color: color.withValues(alpha: 0.15),\n        borderRadius: .circular(10),\n      ),\n      child: Icon(icon, color: color, size: 20),\n    );\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    if (!hasLocation) return;\n\n    if (Preference.notifyEew == null ||\n        Preference.notifyMonitor == null ||\n        Preference.notifyReport == null ||\n        Preference.notifyIntensity == null ||\n        Preference.notifyThunderstorm == null ||\n        Preference.notifyWeatherAdvisory == null ||\n        Preference.notifyEvacuation == null ||\n        Preference.notifyTsunami == null ||\n        Preference.notifyAnnouncement == null) {\n      setState(() => isLoading = true);\n\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) {\n          context.scaffoldMessenger.clearSnackBars();\n          context.scaffoldMessenger.showSnackBar(\n            SnackBar(content: Text('伺服器排隊中，請稍候…'.i18n)),\n          );\n        }\n      });\n      ExpTech()\n          .getNotify(token: Preference.notifyToken)\n          .then((value) {\n            GlobalProviders.notification.apply(value);\n            setState(() => isLoading = false);\n          })\n          .catchError((error) {\n            if (error.toString().contains('401')) {\n              if (GlobalProviders.location.coordinates != null) {\n                Future.delayed(const Duration(seconds: 2), () {\n                  ExpTech()\n                      .updateDeviceLocation(\n                        token: Preference.notifyToken,\n                        coordinates: GlobalProviders.location.coordinates!,\n                      )\n                      .then((_) {\n                        if (mounted) {\n                          context.pop();\n                        }\n                      })\n                      .catchError((updateError) {\n                        TalkerManager.instance.error(\n                          'Failed to update location: $updateError',\n                        );\n                      });\n                });\n              }\n            }\n          });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Selector<SettingsLocationModel, String?>(\n      selector: (_, model) => model.code,\n      builder: (context, code, child) {\n        final enabled = hasLocation;\n\n        return Stack(\n          children: [\n            Positioned(\n              top: 0,\n              left: 0,\n              right: 0,\n              child: AnimatedOpacity(\n                opacity: isLoading && enabled ? 1 : 0,\n                duration: Durations.short4,\n                child: const LinearProgressIndicator(),\n              ),\n            ),\n            ListView(\n              padding: .only(\n                top: 8,\n                bottom: 16 + context.padding.bottom,\n              ),\n              children: [\n                SettingsHeader(\n                  icon: Symbols.notifications_rounded,\n                  title: Text('通知'.i18n),\n                  subtitle: Text('推播通知設定與通知音效測試'.i18n),\n                ),\n                if (!enabled)\n                  Container(\n                    margin: const .fromLTRB(16, 16, 16, 0),\n                    padding: const .all(16),\n                    decoration: BoxDecoration(\n                      color: context.theme.extendedColors.amber.withValues(\n                        alpha: 0.15,\n                      ),\n                      borderRadius: .circular(16),\n                    ),\n                    child: Row(\n                      children: [\n                        Container(\n                          padding: const .all(10),\n                          decoration: BoxDecoration(\n                            color: context.theme.extendedColors.amber.withValues(alpha: 0.2),\n                            borderRadius: .circular(12),\n                          ),\n                          child: Icon(\n                            Symbols.warning_rounded,\n                            color: context.theme.extendedColors.amber,\n                            size: 24,\n                          ),\n                        ),\n                        const SizedBox(width: 16),\n                        Expanded(\n                          child: Column(\n                            crossAxisAlignment: .start,\n                            children: [\n                              Text(\n                                '尚未設定所在地'.i18n,\n                                style: context.texts.titleMedium?.copyWith(\n                                  fontWeight: .w600,\n                                  color: context.theme.extendedColors.amber,\n                                ),\n                              ),\n                              const SizedBox(height: 4),\n                              Text(\n                                '請先設定所在地來使用通知功能'.i18n,\n                                style: context.texts.bodySmall?.copyWith(\n                                  color: context.colors.onSurfaceVariant,\n                                ),\n                              ),\n                              const SizedBox(height: 12),\n                              FilledButton.icon(\n                                onPressed: () => const SettingsLocationRoute().push(context),\n                                icon: const Icon(Symbols.location_on_rounded),\n                                label: Text('設定所在地'.i18n),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                SegmentedList(\n                  label: Text('地震速報'.i18n),\n                  children: [\n                    Selector<SettingsNotificationModel, EewNotifyType>(\n                      selector: (_, model) => model.eew,\n                      builder: (context, eew, child) {\n                        return SegmentedListTile(\n                          isFirst: true,\n                          isLast: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.crisis_alert_rounded,\n                            color: Colors.red,\n                          ),\n                          title: Text('緊急地震速報'.i18n),\n                          subtitle: Text(getEewNotifyTypeName(eew)),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyEewRoute().push(context),\n                        );\n                      },\n                    ),\n                  ],\n                ),\n                SegmentedList(\n                  label: Text('地震'.i18n),\n                  children: [\n                    Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n                      selector: (_, model) => model.monitor,\n                      builder: (context, monitor, child) {\n                        return SegmentedListTile(\n                          isFirst: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.monitor_heart_rounded,\n                            color: Colors.orange,\n                          ),\n                          title: Text('強震監視器'.i18n),\n                          subtitle: Text(\n                            getEarthquakeNotifyTypeName(monitor),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyMonitorRoute().push(context),\n                        );\n                      },\n                    ),\n                    Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n                      selector: (_, model) => model.report,\n                      builder: (context, report, child) {\n                        return SegmentedListTile(\n                          leading: _buildIconContainer(\n                            icon: Symbols.docs_rounded,\n                            color: Colors.blue,\n                          ),\n                          title: Text('地震報告'.i18n),\n                          subtitle: Text(\n                            getEarthquakeNotifyTypeName(report),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyReportRoute().push(context),\n                        );\n                      },\n                    ),\n                    Selector<SettingsNotificationModel, EarthquakeNotifyType>(\n                      selector: (_, model) => model.intensity,\n                      builder: (context, intensity, child) {\n                        return SegmentedListTile(\n                          isLast: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.summarize_rounded,\n                            color: Colors.teal,\n                          ),\n                          title: Text('震度速報'.i18n),\n                          subtitle: Text(\n                            getEarthquakeNotifyTypeName(intensity),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyIntensityRoute().push(context),\n                        );\n                      },\n                    ),\n                  ],\n                ),\n                SegmentedList(\n                  label: Text('天氣'.i18n),\n                  children: [\n                    Selector<SettingsNotificationModel, WeatherNotifyType>(\n                      selector: (_, model) => model.thunderstorm,\n                      builder: (context, thunderstorm, child) {\n                        return SegmentedListTile(\n                          isFirst: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.thunderstorm_rounded,\n                            color: Colors.purple,\n                          ),\n                          title: Text('雷雨即時訊息'.i18n),\n                          subtitle: Text(\n                            getWeatherNotifyTypeName(thunderstorm),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyThunderstormRoute().push(context),\n                        );\n                      },\n                    ),\n                    Selector<SettingsNotificationModel, WeatherNotifyType>(\n                      selector: (_, model) => model.weatherAdvisory,\n                      builder: (context, weatherAdvisory, child) {\n                        return SegmentedListTile(\n                          leading: _buildIconContainer(\n                            icon: Symbols.warning_rounded,\n                            color: Colors.amber,\n                          ),\n                          title: Text('天氣警特報'.i18n),\n                          subtitle: Text(\n                            getWeatherNotifyTypeName(weatherAdvisory),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyAdvisoryRoute().push(context),\n                        );\n                      },\n                    ),\n                    Selector<SettingsNotificationModel, WeatherNotifyType>(\n                      selector: (_, model) => model.evacuation,\n                      builder: (context, evacuation, child) {\n                        return SegmentedListTile(\n                          isLast: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.directions_run_rounded,\n                            color: Colors.green,\n                          ),\n                          title: Text('防災資訊'.i18n),\n                          subtitle: Text(\n                            getWeatherNotifyTypeName(evacuation),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyEvacuationRoute().push(context),\n                        );\n                      },\n                    ),\n                  ],\n                ),\n                SegmentedList(\n                  label: Text('海嘯'.i18n),\n                  children: [\n                    Selector<SettingsNotificationModel, TsunamiNotifyType>(\n                      selector: (_, model) => model.tsunami,\n                      builder: (context, tsunami, child) {\n                        return SegmentedListTile(\n                          isFirst: true,\n                          isLast: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.tsunami_rounded,\n                            color: Colors.cyan,\n                          ),\n                          title: Text('海嘯資訊'.i18n),\n                          subtitle: Text(getTsunamiNotifyTypeName(tsunami)),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyTsunamiRoute().push(context),\n                        );\n                      },\n                    ),\n                  ],\n                ),\n                SegmentedList(\n                  label: Text('其他'.i18n),\n                  children: [\n                    Selector<SettingsNotificationModel, BasicNotifyType>(\n                      selector: (_, model) => model.announcement,\n                      builder: (context, announcement, child) {\n                        return SegmentedListTile(\n                          isFirst: true,\n                          isLast: true,\n                          leading: _buildIconContainer(\n                            icon: Symbols.campaign_rounded,\n                            color: Colors.indigo,\n                          ),\n                          title: Text('公告'.i18n),\n                          subtitle: Text(\n                            getBasicNotifyTypeName(announcement),\n                          ),\n                          trailing: const Icon(Symbols.chevron_right_rounded),\n                          enabled: !isLoading && enabled,\n                          onTap: () => const SettingsNotifyAnnouncementRoute().push(context),\n                        );\n                      },\n                    ),\n                  ],\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/page.dart",
    "content": "/// Settings index page listing all top-level settings categories.\nlibrary;\n\nimport 'package:dpip/core/device_info.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/typography.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:simple_icons/simple_icons.dart';\n\n/// The root settings page.\n///\n/// Displays a scrollable list of setting categories (location, UI, notifications,\n/// network, info, links, and debug). Tap any row to navigate to that section.\nclass SettingsIndexPage extends StatelessWidget {\n  /// Creates a [SettingsIndexPage].\n  const SettingsIndexPage({super.key});\n\n  Widget _buildHeader(BuildContext context) {\n    return Container(\n      margin: const .fromLTRB(16, 0, 16, 8),\n      padding: const .all(20),\n      decoration: BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            context.colors.primaryContainer.withValues(alpha: 0.6),\n            context.colors.tertiaryContainer.withValues(alpha: 0.4),\n          ],\n          begin: .topLeft,\n          end: .bottomRight,\n        ),\n        borderRadius: .circular(20),\n        border: .all(\n          color: context.colors.outlineVariant.withValues(alpha: 0.5),\n        ),\n      ),\n      child: Row(\n        children: [\n          Container(\n            padding: const .all(12),\n            decoration: BoxDecoration(\n              gradient: LinearGradient(\n                colors: [\n                  context.colors.primary,\n                  context.colors.tertiary,\n                ],\n                begin: .topLeft,\n                end: .bottomRight,\n              ),\n              borderRadius: .circular(16),\n              boxShadow: [\n                BoxShadow(\n                  color: context.colors.primary.withValues(alpha: 0.3),\n                  blurRadius: 8,\n                  offset: const Offset(0, 2),\n                ),\n              ],\n            ),\n            child: const Icon(\n              Symbols.settings_rounded,\n              color: Colors.white,\n              size: 28,\n            ),\n          ),\n          const SizedBox(width: 16),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: .start,\n              children: [\n                TitleText.large('設定'.i18n, weight: .bold),\n                BodyText.large('自訂你的 DPIP 使用體驗'.i18n),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final appInfo = '${Global.packageInfo.version}(${Global.packageInfo.buildNumber})';\n    final deviceInfo =\n        '${DeviceInfo.model}${DeviceInfo.serial != null ? '' : ''}(${DeviceInfo.version})';\n\n    return ListView(\n      padding: .only(top: 16, bottom: 16 + context.padding.bottom),\n      children: [\n        _buildHeader(context),\n\n        // 位置\n        SegmentedList(\n          label: Text('位置'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.pin_drop_rounded,\n                color: Colors.deepOrangeAccent,\n              ),\n              title: Text('所在地'.i18n),\n              subtitle: Text('設定你的所在地來接收當地的即時資訊'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsLocationRoute().push(context),\n            ),\n          ],\n        ),\n\n        // 介面\n        SegmentedList(\n          label: Text('介面'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              leading: ContainedIcon(\n                Symbols.dashboard_rounded,\n                color: Colors.lightBlueAccent,\n              ),\n              title: Text('版面'.i18n),\n              subtitle: Text('調整首頁的版面樣式'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsLayoutRoute().push(context),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.palette_rounded,\n                color: Colors.indigoAccent,\n              ),\n              title: Text('主題'.i18n),\n              subtitle: Text('調整 DPIP 整體的外觀與顏色'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsThemeRoute().push(context),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.translate_rounded,\n                color: Colors.tealAccent,\n              ),\n              title: Text('語言'.i18n),\n              subtitle: Text('調整 DPIP 的顯示語言'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsLocaleRoute().push(context),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.percent_rounded,\n                color: Colors.orangeAccent,\n              ),\n              title: Text('單位'.i18n),\n              subtitle: Text('調整 DPIP 顯示數值時使用的單位'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsUnitRoute().push(context),\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.map_rounded,\n                color: Colors.greenAccent,\n              ),\n              title: Text('地圖'.i18n),\n              subtitle: Text('調整地圖的顯示樣式'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsMapRoute().push(context),\n            ),\n          ],\n        ),\n\n        // 通知\n        SegmentedList(\n          label: Text('通知'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.notifications_rounded,\n                color: Colors.amberAccent,\n              ),\n              title: Text('通知'.i18n),\n              subtitle: Text('推播通知設定與通知音效測試'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsNotifyRoute().push(context),\n            ),\n          ],\n        ),\n\n        // 網路\n        SegmentedList(\n          label: Text('網路'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.settings_ethernet_rounded,\n                color: Colors.blueGrey,\n              ),\n              title: Text('HTTP 代理'.i18n),\n              subtitle: Text('調整 HTTP 代理伺服器設定'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsProxyRoute().push(context),\n            ),\n          ],\n        ),\n\n        // 資訊\n        SegmentedList(\n          label: Text('資訊'.i18n),\n          children: [\n            /*SegmentedListTile(\n              isFirst: true,\n              leading: ContainedIcon(\n                Symbols.newspaper_rounded,\n                color: Colors.indigoAccent,\n              ),\n              title: Text('公告'.i18n),\n              subtitle: Text('掌握 ExpTech Studio 的最新公告與資訊'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => AnnouncementRoute().push(context),\n            ),*/\n            SegmentedListTile(\n              isFirst: true, //公告回歸時，要拿掉\n              leading: ContainedIcon(\n                Symbols.update_rounded,\n                color: Colors.cyanAccent,\n              ),\n              title: Text('更新日誌'.i18n),\n              subtitle: Text('瀏覽 DPIP 的歷次更新紀錄'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => ChangelogRoute().push(context),\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.book_rounded,\n                color: Colors.brown,\n              ),\n              title: Text('第三方套件授權'.i18n),\n              subtitle: Text('DPIP 的實現歸功於開放原始碼'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => LicenseRoute().push(context),\n            ),\n          ],\n        ),\n\n        const SizedBox(height: 24),\n\n        // 贊助\n        SegmentedList(\n          children: [\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.volunteer_activism_rounded,\n                color: Colors.black,\n                backgroundGradient: const LinearGradient(\n                  colors: [Color(0xFFFFD700), Color(0xFFFFA500)],\n                  begin: .topLeft,\n                  end: .bottomRight,\n                ),\n              ),\n              title: Text(\n                '贊助我們'.i18n,\n                style: .new(color: Colors.amber[600]),\n              ),\n              subtitle: Text('幫助我們維護伺服器的穩定和長久發展'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              tileColor: Colors.amber.withValues(alpha: 0.16),\n              shape: RoundedRectangleBorder(\n                borderRadius: .circular(20),\n                side: BorderSide(color: Colors.amber.withValues(alpha: 0.6)),\n              ),\n              onTap: () => SettingsDonateRoute().push(context),\n            ),\n          ],\n        ),\n\n        // ExpTech Studio 連結\n        SegmentedList(\n          label: Text('ExpTech Studio'),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              leading: ContainedIcon(\n                SimpleIcons.github,\n                color: switch (context.theme.brightness) {\n                  .light => SimpleIconColors.github,\n                  .dark => SimpleIconColors.github.inverted,\n                },\n              ),\n              title: const Text('Github'),\n              subtitle: const Text('ExpTechTW'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () => 'https://github.com/ExpTechTW/DPIP-Pocket'.launch(),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                SimpleIcons.discord,\n                color: switch (context.theme.brightness) {\n                  .light => .new(0xff454FBF),\n                  .dark => .new(0xff5865F2),\n                },\n              ),\n              title: const Text('Discord'),\n              subtitle: const Text('.gg/exptech-studio'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () => 'https://discord.gg/exptech-studio'.launch(),\n              onLongPress: () => 'https://discord.gg/exptech-studio'.copy(),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                SimpleIcons.threads,\n                color: switch (context.theme.brightness) {\n                  .light => SimpleIconColors.threads,\n                  .dark => SimpleIconColors.threads.inverted,\n                },\n              ),\n              title: const Text('Threads'),\n              subtitle: const Text('@dpip.tw'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () => 'https://www.threads.net/@dpip.tw'.launch(),\n              onLongPress: () => 'https://www.threads.net/@dpip.tw'.copy(),\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                SimpleIcons.youtube,\n                color: SimpleIconColors.youtube,\n              ),\n              title: const Text('Youtube'),\n              subtitle: const Text('@exptechtw'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () => 'https://www.youtube.com/@exptechtw/live'.launch(),\n              onLongPress: () => 'https://www.youtube.com/@exptechtw/live'.copy(),\n            ),\n          ],\n        ),\n\n        // 下載\n        SegmentedList(\n          label: Text('下載'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              leading: ContainedIcon(\n                SimpleIcons.appstore,\n                color: SimpleIconColors.appstore,\n              ),\n              title: const Text('App Store'),\n              subtitle: const Text('iOS'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () => 'https://apps.apple.com/tw/app/dpip/id6468026362'.launch(),\n              onLongPress: () => 'https://apps.apple.com/tw/app/dpip/id6468026362'.copy(),\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                SimpleIcons.googleplay,\n                color: switch (context.theme.brightness) {\n                  .light => SimpleIconColors.googleplay,\n                  .dark => SimpleIconColors.googleplay.inverted,\n                },\n              ),\n              title: const Text('Google Play'),\n              subtitle: const Text('Android'),\n              trailing: const Icon(Symbols.arrow_outward_rounded),\n              onTap: () =>\n                  'https://play.google.com/store/apps/details?id=com.exptech.dpip'.launch(),\n              onLongPress: () =>\n                  'https://play.google.com/store/apps/details?id=com.exptech.dpip'.copy(),\n            ),\n          ],\n        ),\n\n        // 除錯\n        SegmentedList(\n          label: Text('除錯'.i18n),\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              leading: ContainedIcon(\n                Symbols.info_rounded,\n                color: context.colors.onSurfaceVariant,\n              ),\n              title: Text('應用程式版本'.i18n),\n              trailing: Text(appInfo),\n              onLongPress: () => appInfo.copy(),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.smartphone_rounded,\n                color: context.colors.onSurfaceVariant,\n              ),\n              title: Text('裝置資訊'.i18n),\n              trailing: Text(deviceInfo),\n              onLongPress: () => deviceInfo.copy(),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.key_rounded,\n                color: context.colors.onSurfaceVariant,\n              ),\n              title: Text('複製通知 Token'.i18n),\n              trailing: const Icon(Symbols.content_copy_rounded),\n              onTap: () => Preference.notifyToken.copy(),\n            ),\n            SegmentedListTile(\n              leading: ContainedIcon(\n                Symbols.bug_report_rounded,\n                color: context.colors.onSurfaceVariant,\n              ),\n              title: Text('App 日誌'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => AppDebugLogsRoute().push(context),\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.science_rounded,\n                color: context.colors.onSurfaceVariant,\n              ),\n              title: Text('實驗性功能'.i18n),\n              subtitle: Text('搶先體驗開發中的新功能'.i18n),\n              trailing: const Icon(Symbols.chevron_right_rounded),\n              onTap: () => SettingsExperimentalRoute().push(context),\n            ),\n          ],\n        ),\n\n        // Footer\n        _buildFooter(context),\n      ],\n    );\n  }\n\n  Widget _buildFooter(BuildContext context) {\n    return Padding(\n      padding: const .symmetric(horizontal: 16, vertical: 64),\n      child: Column(\n        spacing: 4,\n        children: [\n          Container(\n            height: 84,\n            width: 84,\n            margin: .only(bottom: 16),\n            decoration: BoxDecoration(borderRadius: .circular(24)),\n            clipBehavior: .antiAlias,\n            child: Image.asset('assets/ExpTech.png'),\n          ),\n          TitleText.medium(\n            'ExpTech Studio © 2026',\n            color: context.colors.onSurfaceVariant,\n            weight: .bold,\n            align: .center,\n          ),\n          BodyText.medium(\n            '任何資訊應以中央氣象署發布之內容為準'.i18n,\n            color: context.colors.outline,\n            align: .center,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/proxy/page.dart",
    "content": "/// HTTP proxy settings page.\nlibrary;\n\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A settings page for configuring an HTTP proxy server.\n///\n/// Changes are saved immediately; a restart is required for them to take\n/// effect.\nclass SettingsProxyPage extends StatefulWidget {\n  /// Creates a [SettingsProxyPage].\n  const SettingsProxyPage({super.key});\n\n  @override\n  State<SettingsProxyPage> createState() => _SettingsProxyPageState();\n}\n\nclass _SettingsProxyPageState extends State<SettingsProxyPage> {\n  late final TextEditingController _hostController;\n  late final TextEditingController _portController;\n  late bool _enabled;\n\n  void _saveSettings() {\n    Preference.proxyEnabled = _enabled;\n    Preference.proxyHost = _hostController.text.trim().isEmpty ? null : _hostController.text.trim();\n    final portText = _portController.text.trim();\n    Preference.proxyPort = portText.isEmpty ? null : int.tryParse(portText);\n\n    if (mounted) {\n      context.scaffoldMessenger.showSnackBar(\n        SnackBar(\n          content: Text('設定已儲存'.i18n),\n          duration: const Duration(seconds: 2),\n        ),\n      );\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _enabled = Preference.proxyEnabled ?? false;\n    _hostController = TextEditingController(\n      text: Preference.proxyHost ?? 'localhost',\n    );\n    _portController = TextEditingController(\n      text: Preference.proxyPort?.toString() ?? '9090',\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: .only(top: 8, bottom: 16 + context.padding.bottom),\n      children: [\n        SettingsHeader(\n          icon: Symbols.settings_ethernet_rounded,\n          title: Text('HTTP 代理'.i18n),\n          subtitle: Text('調整 HTTP 代理伺服器設定'.i18n),\n        ),\n        const SizedBox(height: 16),\n        SegmentedList(\n          children: [\n            SegmentedListTile(\n              isFirst: true,\n              isLast: !_enabled,\n              leading: Icon(Symbols.settings_ethernet_rounded),\n              title: Text('啟用代理'.i18n),\n              subtitle: Text('透過代理伺服器發送所有網路請求'.i18n),\n              trailing: Switch(\n                value: _enabled,\n                onChanged: (value) {\n                  setState(() => _enabled = value);\n                  _saveSettings();\n                },\n              ),\n            ),\n            if (_enabled) ...[\n              SegmentedListTile(\n                leading: Icon(Symbols.host_rounded),\n                title: Text('代理主機'.i18n),\n                content: TextField(\n                  controller: _hostController,\n                  decoration: InputDecoration(\n                    hintText: 'localhost',\n                    border: OutlineInputBorder(borderRadius: .circular(8)),\n                    visualDensity: .compact,\n                  ),\n                  onChanged: (_) => _saveSettings(),\n                ),\n              ),\n              SegmentedListTile(\n                isLast: true,\n                leading: Icon(Symbols.settings_ethernet_rounded),\n                title: Text('代理端口'.i18n),\n                content: TextField(\n                  controller: _portController,\n                  decoration: InputDecoration(\n                    hintText: '9090',\n                    border: OutlineInputBorder(borderRadius: .circular(8)),\n                    visualDensity: .compact,\n                  ),\n                  keyboardType: TextInputType.number,\n                  onChanged: (_) => _saveSettings(),\n                ),\n              ),\n            ],\n          ],\n        ),\n        if (_enabled) SectionText(child: Text('設定儲存後，需要重新啟動應用程式才能生效'.i18n)),\n      ],\n    );\n  }\n\n  @override\n  void dispose() {\n    _hostController.dispose();\n    _portController.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/theme/color/page.dart",
    "content": "/// Theme color settings page.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/color_picker.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:dynamic_color/dynamic_color.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A page for selecting the app accent color.\n///\n/// Offers a system dynamic color option and a custom color picker.\n/// Requires [SettingsUserInterfaceModel] via [BuildContext] extensions.\nclass SettingsThemeColorPage extends StatelessWidget {\n  /// Creates a [SettingsThemeColorPage].\n  const SettingsThemeColorPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final color = context.useUserInterface.themeColor;\n\n    return ListView(\n      padding: .only(bottom: context.padding.bottom + 64),\n      children: [\n        SegmentedList(\n          label: Text('主題色彩'.i18n),\n          children: [\n            DynamicColorBuilder(\n              builder: (lightDynamic, darkDynamic) {\n                final seed = switch (context.theme.brightness) {\n                  .light => lightDynamic ?? .light(),\n                  .dark => darkDynamic ?? .dark(),\n                }.primary;\n\n                final colors = ColorScheme.fromSeed(\n                  seedColor: seed,\n                  brightness: context.theme.brightness,\n                );\n\n                return SegmentedListTile(\n                  isFirst: true,\n                  leading: ContainedIcon(\n                    Symbols.devices_rounded,\n                    color: colors.primary,\n                  ),\n                  title: Text(\n                    '使用系統配色'.i18n,\n                    style: TextStyle(color: colors.onSurface),\n                  ),\n                  tileColor: colors.surfaceContainerHigh,\n                  subtitle: Text(\n                    '#${colors.primary.toHexString()}',\n                    style: TextStyle(color: colors.onSurfaceVariant),\n                  ),\n                  trailing: color == null ? Icon(Symbols.check_rounded) : null,\n                  onTap: () => context.userInterface.setThemeColor(null),\n                );\n              },\n            ),\n            SegmentedListTile(\n              isLast: true,\n              leading: ContainedIcon(\n                Symbols.format_paint_rounded,\n                color: color,\n              ),\n              title: Text('自訂'.i18n),\n              subtitle: color != null ? Text('#${color.toHexString()}') : null,\n              trailing: color != null ? Icon(Symbols.check_rounded) : null,\n              onTap: () => context.userInterface.setThemeColor(context.colors.primary),\n            ),\n          ],\n        ),\n        if (color != null)\n          SegmentedList(\n            label: Text('自訂色彩'.i18n),\n            children: [\n              ColorPicker(\n                color: .fromColor(color),\n                onChanged: (value) {\n                  context.userInterface.setThemeColor(value.toColor());\n                },\n              ),\n            ],\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/theme/mode/page.dart",
    "content": "/// Theme mode settings page.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A page for selecting the app theme mode (light, dark, or system).\n///\n/// Selecting a mode immediately applies it and pops the page. Requires\n/// [SettingsUserInterfaceModel] in the widget tree.\nclass SettingsThemeModePage extends StatelessWidget {\n  /// Creates a [SettingsThemeModePage].\n  const SettingsThemeModePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: .only(top: 8, bottom: 16 + context.padding.bottom),\n      children: [\n        SegmentedList(\n          label: Text('主題模式'.i18n),\n          children: [\n            Selector<SettingsUserInterfaceModel, ThemeMode>(\n              selector: (context, model) => model.themeMode,\n              builder: (context, themeMode, child) => SegmentedListTile(\n                isFirst: true,\n                leading: Icon(Symbols.light_mode_rounded),\n                title: Text('淺色'.i18n),\n                trailing: Icon(\n                  themeMode == .light ? Symbols.check_rounded : null,\n                ),\n                onTap: () {\n                  context.read<SettingsUserInterfaceModel>().setThemeMode(\n                    .light,\n                  );\n                  context.pop();\n                },\n              ),\n            ),\n            Selector<SettingsUserInterfaceModel, ThemeMode>(\n              selector: (context, model) => model.themeMode,\n              builder: (context, themeMode, child) => SegmentedListTile(\n                leading: Icon(Symbols.dark_mode_rounded),\n                title: Text('深色'.i18n),\n                trailing: Icon(\n                  themeMode == .dark ? Symbols.check_rounded : null,\n                ),\n                onTap: () {\n                  context.read<SettingsUserInterfaceModel>().setThemeMode(\n                    .dark,\n                  );\n                  context.pop();\n                },\n              ),\n            ),\n            Selector<SettingsUserInterfaceModel, ThemeMode>(\n              selector: (context, model) => model.themeMode,\n              builder: (context, themeMode, child) => SegmentedListTile(\n                isLast: true,\n                leading: Icon(Symbols.devices_rounded),\n                title: Text('跟隨系統主題'.i18n),\n                subtitle: Text(switch (context.brightness) {\n                  .light => '淺色'.i18n,\n                  .dark => '深色'.i18n,\n                }),\n                trailing: Icon(\n                  themeMode == .system ? Symbols.check_rounded : null,\n                ),\n                onTap: () {\n                  context.read<SettingsUserInterfaceModel>().setThemeMode(\n                    .system,\n                  );\n                  context.pop();\n                },\n              ),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/theme/page.dart",
    "content": "/// Theme settings page for adjusting the app's visual appearance.\nlibrary;\n\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/theme.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flex_color_picker/flex_color_picker.dart' show ColorTools;\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A settings page for choosing the theme mode and accent color.\n///\n/// Requires [SettingsUserInterfaceModel] in the widget tree.\nclass SettingsThemePage extends StatelessWidget {\n  /// Creates a [SettingsThemePage].\n  const SettingsThemePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<SettingsUserInterfaceModel>(\n      builder: (context, model, child) {\n        return ListView(\n          children: [\n            SettingsHeader(\n              icon: Symbols.palette_rounded,\n              title: Text('主題'.i18n),\n              subtitle: Text('調整 DPIP 整體的外觀與顏色'.i18n),\n            ),\n            const SizedBox(height: 16),\n            SegmentedList(\n              children: [\n                Selector<SettingsUserInterfaceModel, ThemeMode>(\n                  selector: (context, model) => model.themeMode,\n                  builder: (context, themeMode, child) {\n                    return SegmentedListTile(\n                      isFirst: true,\n                      leading: ContainedIcon(\n                        switch (context.theme.brightness) {\n                          .light => Symbols.light_mode_rounded,\n                          .dark => Symbols.dark_mode_rounded,\n                        },\n                        color: switch (context.theme.brightness) {\n                          .light => Colors.orange,\n                          .dark => Colors.blue[300]!,\n                        },\n                      ),\n                      title: Text('主題模式'.i18n),\n                      subtitle: Text(themeMode.label.i18n),\n                      trailing: const Icon(Symbols.chevron_right_rounded),\n                      onTap: () => const SettingsThemeModeRoute().push(context),\n                    );\n                  },\n                ),\n                Selector<SettingsUserInterfaceModel, Color?>(\n                  selector: (context, model) => model.themeColor,\n                  builder: (context, themeColor, child) {\n                    return SegmentedListTile(\n                      isLast: true,\n                      leading: ContainedIcon(\n                        Symbols.colorize_rounded,\n                        color: themeColor ?? context.colors.primary,\n                      ),\n                      title: Text('主題色彩'.i18n),\n                      subtitle: Text(switch (themeColor) {\n                        null => '使用系統配色'.i18n,\n                        final v => ColorTools.nameThatColor(v),\n                      }),\n                      trailing: const Icon(Symbols.chevron_right_rounded),\n                      onTap: () => const SettingsThemeColorRoute().push(context),\n                    );\n                  },\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/settings/unit/page.dart",
    "content": "/// Settings page for configuring measurement units displayed throughout the app.\n///\n/// Unit preferences are persisted through [SettingsUserInterfaceModel] via\n/// [Provider].\nlibrary;\n\nimport 'package:dpip/app/settings/_widgets/settings_header.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/segmented_list.dart';\nimport 'package:dpip/widgets/ui/icon_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:provider/provider.dart';\n\n/// A settings page for measurement unit preferences.\n///\n/// Renders controls for each supported unit option. Requires\n/// [SettingsUserInterfaceModel] to be available in the widget tree:\n///\n/// ```dart\n/// ChangeNotifierProvider(\n///   create: (_) => SettingsUserInterfaceModel(),\n///   child: const SettingsUnitPage(),\n/// )\n/// ```\nclass SettingsUnitPage extends StatelessWidget {\n  /// Creates a [SettingsUnitPage].\n  const SettingsUnitPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<SettingsUserInterfaceModel>(\n      builder: (context, model, child) {\n        return ListView(\n          padding: .only(\n            top: 16,\n            bottom: 16 + context.padding.bottom,\n          ),\n          children: [\n            SettingsHeader(\n              icon: Symbols.straighten_rounded,\n              title: Text('單位'.i18n),\n              subtitle: Text('調整 DPIP 顯示數值時使用的單位'.i18n),\n            ),\n            const SizedBox(height: 16),\n            SegmentedList(\n              children: [\n                Selector<SettingsUserInterfaceModel, bool>(\n                  selector: (context, model) => model.useFahrenheit,\n                  builder: (context, useFahrenheit, child) {\n                    return SegmentedListTile(\n                      isFirst: true,\n                      isLast: true,\n                      leading: ContainedIcon(\n                        Symbols.thermostat_rounded,\n                        color: Colors.amberAccent,\n                      ),\n                      title: Text('使用華氏度'.i18n),\n                      subtitle: Text('切換溫度顯示單位為華氏度 (℉)'.i18n),\n                      trailing: Switch(\n                        value: useFahrenheit,\n                        onChanged: (value) => model.setUseFahrenheit(value),\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/welcome/1-about/page.dart",
    "content": "/// The first welcome step, introducing the DPIP app.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// Displays the \"About DPIP\" introduction page in the welcome flow.\n///\n/// Tapping the next button navigates to [WelcomeExptechRoute].\nclass WelcomeAboutPage extends StatelessWidget {\n  /// Creates a [WelcomeAboutPage].\n  const WelcomeAboutPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      bottomNavigationBar: SafeArea(\n        child: Padding(\n          padding: const .symmetric(horizontal: 24, vertical: 8),\n          child: FilledButton(\n            child: Text('下一步'.i18n),\n            onPressed: () => WelcomeExptechRoute().push(context),\n          ),\n        ),\n      ),\n      body: SingleChildScrollView(\n        padding: context.padding,\n        child: Column(\n          crossAxisAlignment: .stretch,\n          children: [\n            Padding(\n              padding: const .fromLTRB(0, 32, 0, 16),\n              child: Column(\n                children: [\n                  Padding(\n                    padding: const .all(16),\n                    child: ClipRRect(\n                      borderRadius: .circular(16),\n                      child: Image.asset(\n                        'assets/DPIP.png',\n                        width: 120,\n                        height: 120,\n                      ),\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(16),\n                    child: Text(\n                      '歡迎使用 DPIP'.i18n,\n                      style: context.texts.headlineMedium?.copyWith(\n                        fontWeight: .bold,\n                        color: context.colors.primary,\n                      ),\n                      textAlign: .center,\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(8),\n                    child: Column(\n                      children: [\n                        Text(\n                          'Disaster Prevention Information Platform',\n                          style: context.texts.titleMedium?.copyWith(\n                            color: context.colors.primary.withValues(\n                              alpha: 0.7,\n                            ),\n                          ),\n                          textAlign: .center,\n                        ),\n                        Text(\n                          '防災資訊平台'.i18n,\n                          style: context.texts.titleMedium?.copyWith(\n                            color: context.colors.primary.withValues(\n                              alpha: 0.7,\n                            ),\n                          ),\n                          textAlign: .center,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            Padding(\n              padding: const .all(16),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  'DPIP 是一款由臺灣本土團隊設計的 App，整合 TREM-Net (臺灣即時地震觀測網) 之資訊，以及中央氣象署資料，提供一個整合、單一且便利的防災資訊應用程式。'\n                      .i18n,\n                  style: context.texts.bodyLarge,\n                  textAlign: .left,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/welcome/2-exptech/page.dart",
    "content": "/// The second welcome step, introducing ExpTech Studio.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// Displays the \"About ExpTech Studio\" page in the welcome flow.\n///\n/// Tapping the next button navigates to [WelcomeNoticeRoute].\nclass WelcomeExpTechPage extends StatelessWidget {\n  /// Creates a [WelcomeExpTechPage].\n  const WelcomeExpTechPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      bottomNavigationBar: SafeArea(\n        child: Padding(\n          padding: const .symmetric(horizontal: 24, vertical: 8),\n          child: FilledButton(\n            child: Text('下一步'.i18n),\n            onPressed: () => WelcomeNoticeRoute().push(context),\n          ),\n        ),\n      ),\n      body: SingleChildScrollView(\n        padding: context.padding,\n        child: Column(\n          crossAxisAlignment: .stretch,\n          children: [\n            Padding(\n              padding: const .fromLTRB(0, 32, 0, 16),\n              child: Column(\n                children: [\n                  Padding(\n                    padding: const .all(16),\n                    child: ClipRRect(\n                      borderRadius: .circular(16),\n                      child: Image.asset(\n                        'assets/ExpTech.png',\n                        width: 120,\n                        height: 120,\n                      ),\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(16),\n                    child: Text(\n                      'ExpTech Studio',\n                      style: context.texts.headlineMedium?.copyWith(\n                        fontWeight: .bold,\n                        color: context.colors.primary,\n                      ),\n                      textAlign: .center,\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(8),\n                    child: Column(\n                      children: [\n                        Text(\n                          '©2024 ExpTech Studio Ltd.',\n                          style: context.texts.titleMedium?.copyWith(\n                            color: context.colors.primary.withValues(\n                              alpha: 0.7,\n                            ),\n                          ),\n                          textAlign: .center,\n                        ),\n                        Text(\n                          '防災資訊平台'.i18n,\n                          style: context.texts.titleMedium?.copyWith(\n                            color: context.colors.primary.withValues(\n                              alpha: 0.7,\n                            ),\n                          ),\n                          textAlign: .center,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            Padding(\n              padding: const .all(16),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Column(\n                  children: [\n                    Text(\n                      '我們是誰？'.i18n,\n                      style: context.texts.titleLarge?.copyWith(\n                        fontWeight: .bold,\n                      ),\n                    ),\n                    const SizedBox(height: 8),\n                    Text(\n                      'ExpTech Studio 是一群大部分由學生組成，平均年齡未滿 20 歲、人數超過 15 + 的團體。成員來自臺灣北中南、日本、韓國、中國的學生。'\n                          .i18n,\n                      style: context.texts.bodyMedium,\n                    ),\n                    const SizedBox(height: 16),\n                    Text(\n                      '我們的初衷'.i18n,\n                      style: context.texts.titleLarge?.copyWith(\n                        fontWeight: .bold,\n                      ),\n                    ),\n                    const SizedBox(height: 8),\n                    Text(\n                      '成立初衷是招募一群對電腦及科技有興趣及能力的同學，後來發展至校外，並逐漸形成現在的樣子。'.i18n,\n                      style: context.texts.bodyMedium,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/welcome/3-notice/page.dart",
    "content": "/// The third welcome step, displaying important usage notices.\nlibrary;\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// Displays legal and safety notices the user should read before using DPIP.\n///\n/// Tapping the next button navigates to [WelcomePermissionsRoute].\nclass WelcomeNoticePage extends StatelessWidget {\n  /// Creates a [WelcomeNoticePage].\n  const WelcomeNoticePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      bottomNavigationBar: SafeArea(\n        child: Padding(\n          padding: const .symmetric(horizontal: 24, vertical: 8),\n          child: FilledButton(\n            child: Text('下一步'.i18n),\n            onPressed: () => WelcomePermissionsRoute().push(context),\n          ),\n        ),\n      ),\n      body: SingleChildScrollView(\n        padding: context.padding,\n        child: Column(\n          crossAxisAlignment: .stretch,\n          children: [\n            Padding(\n              padding: const .fromLTRB(0, 32, 0, 16),\n              child: Column(\n                children: [\n                  Padding(\n                    padding: const .all(16),\n                    child: Icon(\n                      Symbols.warning_rounded,\n                      size: 80,\n                      color: context.colors.primary,\n                      fill: 1,\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(16),\n                    child: Text(\n                      '注意事項'.i18n,\n                      style: context.texts.headlineMedium?.copyWith(\n                        fontWeight: .bold,\n                        color: context.colors.primary,\n                      ),\n                      textAlign: .center,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            Padding(\n              padding: const .symmetric(horizontal: 16, vertical: 8),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.errorContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  '任何資訊應以中央氣象署發布之內容為準。'.i18n,\n                  style: context.texts.bodyLarge!.copyWith(\n                    color: context.colors.onErrorContainer,\n                    fontWeight: .bold,\n                  ),\n                ),\n              ),\n            ),\n            Padding(\n              padding: const .symmetric(horizontal: 16, vertical: 8),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  '根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等，有收不到資訊的可能性，我們會盡力避免此類情況，但不保證一定不會發生。'.i18n,\n                  style: context.texts.bodyLarge,\n                ),\n              ),\n            ),\n            Padding(\n              padding: const .symmetric(horizontal: 16, vertical: 8),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  '強烈搖晃有機率比通知早抵達使用者所在地。'.i18n,\n                  style: context.texts.bodyLarge,\n                ),\n              ),\n            ),\n            Padding(\n              padding: const .symmetric(horizontal: 16, vertical: 8),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  '地震速報為快速計算之結果，可能存在較大誤差，應理解並謹慎使用。'.i18n,\n                  style: context.texts.bodyLarge,\n                ),\n              ),\n            ),\n            Padding(\n              padding: const .symmetric(horizontal: 16, vertical: 8),\n              child: Container(\n                padding: const .all(16),\n                decoration: BoxDecoration(\n                  color: context.colors.surfaceContainer,\n                  borderRadius: .circular(16),\n                ),\n                child: Text(\n                  '任何不被官方所認可的行為均有可能承擔法律風險，請務必遵守相關規範。'.i18n,\n                  style: context.texts.bodyLarge,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app/welcome/4-permissions/page.dart",
    "content": "/// The fourth welcome step, requesting runtime permissions.\nlibrary;\n\nimport 'dart:io';\n\nimport 'package:awesome_notifications/awesome_notifications.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:firebase_messaging/firebase_messaging.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:permission_handler/permission_handler.dart';\n\n/// Guides the user through granting the permissions required by DPIP.\n///\n/// Displays a list of [PermissionItem] cards for each required permission.\n/// Tapping \"next\" requests notification permission and navigates to the home\n/// screen, marking the first-launch flag as complete.\nclass WelcomePermissionPage extends StatefulWidget {\n  /// Creates a [WelcomePermissionPage].\n  const WelcomePermissionPage({super.key});\n\n  @override\n  State<WelcomePermissionPage> createState() => _WelcomePermissionPageState();\n}\n\nclass _WelcomePermissionPageState extends State<WelcomePermissionPage> with WidgetsBindingObserver {\n  late Future<List<Permission>> _permissionsFuture;\n  late Future<bool> _autoStartPermission;\n  bool _autoStartStatus = false;\n  bool _isRequestingPermission = false;\n  bool _isNotificationPermission = false;\n\n  Future<void> _checkNotificationPermission() async {\n    if (Platform.isAndroid) {\n      final status = await Permission.notification.status;\n      _isNotificationPermission = status.isGranted;\n    } else if (Platform.isIOS) {\n      _isNotificationPermission = await AwesomeNotifications().isNotificationAllowed();\n    }\n  }\n\n  Future<List<Permission>> _initializePermissions() async {\n    final deviceInfo = DeviceInfoPlugin();\n    List<Permission> permissions = [];\n\n    try {\n      final PermissionStatus status = await Permission.location.status;\n      if (status.isGranted) {\n        if (Platform.isAndroid) {\n          final androidInfo = await deviceInfo.androidInfo;\n          permissions = [\n            Permission.notification,\n            Permission.locationAlways,\n            if (androidInfo.version.sdkInt <= 28) Permission.storage,\n            Permission.ignoreBatteryOptimizations,\n          ];\n        } else if (Platform.isIOS) {\n          permissions = [\n            Permission.notification,\n            Permission.locationAlways,\n            Permission.photosAddOnly,\n          ];\n        }\n      } else {\n        if (Platform.isAndroid) {\n          final androidInfo = await deviceInfo.androidInfo;\n          permissions = [\n            Permission.notification,\n            Permission.location,\n            if (androidInfo.version.sdkInt <= 28) Permission.storage,\n            Permission.ignoreBatteryOptimizations,\n          ];\n        } else if (Platform.isIOS) {\n          permissions = [\n            Permission.notification,\n            Permission.location,\n            Permission.photosAddOnly,\n          ];\n        }\n      }\n\n      await _checkNotificationPermission();\n    } catch (e) {\n      TalkerManager.instance.error('Error initializing permissions: $e');\n    }\n\n    return permissions;\n  }\n\n  List<PermissionItem> _createPermissionItems(\n    List<Permission> permissions,\n    BuildContext context,\n  ) {\n    final items = <PermissionItem>[];\n    for (final Permission permission in permissions) {\n      IconData icon;\n      String text;\n      String description;\n      Color color;\n      bool isHighlighted = false;\n\n      switch (permission) {\n        case Permission.notification:\n          icon = Icons.notifications;\n          text = '通知'.i18n;\n          description = '在重大災害發生時以通知來傳遞即時防災資訊'.i18n;\n          color = Colors.orange;\n          isHighlighted = true;\n\n        case Permission.locationAlways:\n        case Permission.location:\n          icon = Icons.location_on;\n          text = '位置'.i18n;\n          description = '使用定位來自動更新所在地設定，提供當地的即時防災資訊'.i18n;\n          color = Colors.blue;\n\n        case Permission.ignoreBatteryOptimizations:\n          icon = Icons.battery_full;\n          text = '省電策略'.i18n;\n          description = '允許 DPIP 在背景中持續運行，以便即時防災通知資訊。'.i18n;\n          color = Colors.greenAccent;\n\n        case Permission.storage:\n        case Permission.photosAddOnly:\n          icon = Platform.isAndroid ? Icons.storage : Icons.photo_library;\n          text = '儲存'.i18n;\n          description = '用於儲存中央氣象署或 ExpTech 提供之資料視覺化圖片'.i18n;\n          color = Colors.green;\n\n        default:\n          continue;\n      }\n\n      items.add(\n        PermissionItem(\n          icon: icon,\n          text: text,\n          description: description,\n          color: color,\n          permission: permission,\n          isHighlighted: isHighlighted,\n        ),\n      );\n    }\n    return items;\n  }\n\n  Widget _buildPermissionCard(PermissionItem item) {\n    return Padding(\n      padding: const .symmetric(horizontal: 16, vertical: 8),\n      child: Container(\n        decoration: BoxDecoration(\n          color: context.colors.surfaceContainer,\n          borderRadius: .circular(16),\n          border: item.isHighlighted ? Border.all(color: Colors.red, width: 2) : null,\n        ),\n        child: ListTile(\n          leading: CircleAvatar(\n            backgroundColor: item.color.withValues(alpha: 0.1),\n            child: Icon(item.icon, color: item.color),\n          ),\n          title: Text(item.text),\n          subtitle: Text(item.description),\n          trailing: _buildPermissionSwitch(item),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPermissionSwitch(PermissionItem item) {\n    return FutureBuilder<PermissionStatus>(\n      future: item.permission.status,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == .waiting) {\n          return const SizedBox(\n            width: 24,\n            height: 24,\n            child: CircularProgressIndicator(strokeWidth: 2),\n          );\n        }\n        final status = snapshot.data ?? PermissionStatus.denied;\n\n        return Switch(\n          value: status.isGranted,\n          onChanged: (value) => _handlePermissionChange(item, value),\n        );\n      },\n    );\n  }\n\n  Future<void> _handlePermissionChange(PermissionItem item, bool value) async {\n    if (_isRequestingPermission) return;\n\n    setState(() {\n      _isRequestingPermission = true;\n    });\n\n    try {\n      if (value) {\n        await _requestPermission(item);\n      } else {\n        await openAppSettings();\n      }\n\n      setState(() {});\n    } catch (e) {\n      if (mounted) {\n        context.scaffoldMessenger.showSnackBar(\n          SnackBar(content: Text('權限請求失敗: ${item.text}')),\n        );\n      }\n    } finally {\n      setState(() {\n        _isRequestingPermission = false;\n      });\n    }\n  }\n\n  Future<void> _requestPermission(PermissionItem item) async {\n    PermissionStatus status;\n\n    switch (item.permission) {\n      case Permission.notification:\n        await _requestNotificationPermission();\n\n      case Permission.location:\n        status = await Permission.location.request();\n\n        if (status.isPermanentlyDenied) {\n          _showPermanentlyDeniedDialog(item);\n        } else if (status.isGranted) {\n          if (Platform.isAndroid) {\n            final shouldContinue = await _showBackgroundLocationExplanationDialog();\n\n            if (shouldContinue && mounted) {\n              await Permission.locationAlways.request();\n            }\n          }\n          _permissionsFuture = _initializePermissions();\n        }\n\n      case Permission.locationAlways:\n        final shouldContinue = await _showBackgroundLocationExplanationDialog();\n\n        if (shouldContinue && mounted) {\n          await Permission.locationAlways.request();\n        }\n\n      case Permission.ignoreBatteryOptimizations:\n      case Permission.storage:\n      case Permission.photosAddOnly:\n        status = await item.permission.request();\n\n        if (status.isPermanentlyDenied) {\n          _showPermanentlyDeniedDialog(item);\n        }\n\n      default:\n    }\n  }\n\n  Future<void> _requestNotificationPermission() async {\n    if (Platform.isAndroid) {\n      final status = await Permission.notification.request();\n      if (status.isGranted) {\n        _isNotificationPermission = true;\n      } else if (status.isPermanentlyDenied) {\n        await openAppSettings();\n      }\n    } else if (Platform.isIOS) {\n      final NotificationSettings iosSettings = await FirebaseMessaging.instance.requestPermission(\n        announcement: true,\n        carPlay: true,\n        criticalAlert: true,\n        provisional: false,\n      );\n      if (iosSettings.criticalAlert == AppleNotificationSetting.enabled) {\n        _isNotificationPermission = true;\n      }\n    }\n  }\n\n  void _showPermanentlyDeniedDialog(PermissionItem item) {\n    showDialog(\n      context: context,\n      builder: (BuildContext context) => AlertDialog(\n        title: Text('權限請求'.i18n),\n        content: Text('需要使用者手動到設定開啟相關權限。'.i18n),\n        actions: [\n          TextButton(\n            child: Text('取消'.i18n),\n            onPressed: () => context.pop(),\n          ),\n          TextButton(\n            child: Text('確定'.i18n),\n            onPressed: () {\n              openAppSettings();\n              context.pop();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<bool> _showBackgroundLocationExplanationDialog() async {\n    final result = await showDialog<bool>(\n      context: context,\n      barrierDismissible: false,\n      builder: (BuildContext context) => AlertDialog(\n        title: Text('需要背景位置權限'.i18n),\n        content: Text(\n          '為了在背景持續提供即時防災資訊，DPIP 需要「永遠允許」位置權限。\\n\\n'\n                  '接下來系統會引導您到設定頁面，請選擇「永遠允許」選項。'\n              .i18n,\n        ),\n        actions: [\n          TextButton(\n            child: Text('稍後'.i18n),\n            onPressed: () => context.pop(false),\n          ),\n          FilledButton(\n            child: Text('前往設定'.i18n),\n            onPressed: () => context.pop(true),\n          ),\n        ],\n      ),\n    );\n    return result ?? false;\n  }\n\n  /// Requests notification permission and navigates to the home screen.\n  ///\n  /// On iOS, also requests critical-alert permission via Firebase Messaging.\n  /// Sets [Preference.isFirstLaunch] to `false` before navigating.\n  Future<void> getNotify() async {\n    if (!_isNotificationPermission) {\n      await Permission.notification.request();\n      if (Platform.isIOS) {\n        final NotificationSettings iosrp = await FirebaseMessaging.instance.requestPermission(\n          announcement: true,\n          carPlay: true,\n          criticalAlert: true,\n          provisional: true,\n        );\n        if (iosrp.criticalAlert == AppleNotificationSetting.enabled) {\n          _isNotificationPermission = true;\n        }\n      }\n    }\n    if (mounted) {\n      Preference.isFirstLaunch = false;\n      HomeRoute().go(context);\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _permissionsFuture = _initializePermissions();\n    // if (Platform.isAndroid) {\n    //   _autoStartStatusCheck();\n    // }\n    WidgetsBinding.instance.addPostFrameCallback((_) async {\n      await _checkNotificationPermission();\n      setState(() {});\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      bottomNavigationBar: SafeArea(\n        child: Padding(\n          padding: const .symmetric(horizontal: 24, vertical: 8),\n          child: FilledButton(onPressed: getNotify, child: Text('下一步'.i18n)),\n        ),\n      ),\n      body: SingleChildScrollView(\n        padding: context.padding,\n        child: Column(\n          crossAxisAlignment: .stretch,\n          children: [\n            Padding(\n              padding: const .fromLTRB(0, 32, 0, 16),\n              child: Column(\n                children: [\n                  Padding(\n                    padding: const .all(16),\n                    child: Icon(\n                      Symbols.security_rounded,\n                      size: 80,\n                      color: context.colors.primary,\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(16),\n                    child: Text(\n                      '權限'.i18n,\n                      style: context.texts.headlineMedium?.copyWith(\n                        fontWeight: .bold,\n                        color: context.colors.primary,\n                      ),\n                      textAlign: .center,\n                    ),\n                  ),\n                  Padding(\n                    padding: const .all(8),\n                    child: Column(\n                      children: [\n                        Text(\n                          '我們一直和使用者站在一起，為使用者的隱私而不斷努力。'.i18n,\n                          style: context.texts.titleMedium?.copyWith(\n                            color: context.colors.primary.withValues(\n                              alpha: 0.7,\n                            ),\n                          ),\n                          textAlign: .center,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            FutureBuilder<List<Permission>>(\n              future: _permissionsFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == .waiting) {\n                  return const Center(child: CircularProgressIndicator());\n                } else if (snapshot.hasError) {\n                  return Center(child: Text('Error: ${snapshot.error}'));\n                } else if (!snapshot.hasData || snapshot.data!.isEmpty) {\n                  return const Center(child: Text('No permissions to display'));\n                }\n\n                final permissionItems = _createPermissionItems(\n                  snapshot.data!,\n                  context,\n                );\n                return Column(\n                  children: permissionItems.map(_buildPermissionCard).toList(),\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    if (state == .resumed) {\n      setState(() {\n        _permissionsFuture = _initializePermissions();\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n}\n\n/// A data class describing a single permission entry in the welcome flow.\n///\n/// Used by [_WelcomePermissionPageState] to build the permission list UI.\nclass PermissionItem {\n  /// The icon representing the permission category.\n  final IconData icon;\n\n  /// The display name of the permission.\n  final String text;\n\n  /// A user-facing explanation of why the permission is needed.\n  final String description;\n\n  /// The accent color used for the permission's icon avatar.\n  final Color color;\n\n  /// The underlying [Permission] object used to query and request status.\n  Permission permission;\n\n  /// Whether this permission has been granted by the user.\n  bool isGranted;\n\n  /// Whether this permission should be visually highlighted as important.\n  bool isHighlighted;\n\n  /// Creates a [PermissionItem] with the given properties.\n  PermissionItem({\n    required this.icon,\n    required this.text,\n    required this.description,\n    required this.color,\n    required this.permission,\n    this.isGranted = false,\n    required this.isHighlighted,\n  });\n}\n"
  },
  {
    "path": "lib/app/welcome/layout.dart",
    "content": "/// The shared scaffold layout used across all welcome flow pages.\nlibrary;\n\nimport 'package:flutter/material.dart';\n\n/// Wraps a [child] widget in a [Scaffold] for the welcome onboarding flow.\n///\n/// Use this as the root widget for each welcome step to get a consistent\n/// page chrome without extra configuration.\nclass WelcomeLayout extends StatelessWidget {\n  /// The content to display inside the scaffold body.\n  final Widget child;\n\n  /// Creates a [WelcomeLayout] wrapping the given [child].\n  const WelcomeLayout({super.key, required this.child});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(body: child);\n  }\n}\n"
  },
  {
    "path": "lib/app.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/core/notify.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/models/settings/ui.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dynamic_color/dynamic_color.dart';\nimport 'package:firebase_messaging/firebase_messaging.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/google_fonts.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:in_app_update/in_app_update.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:provider/provider.dart';\n\nimport 'main.dart';\n\n/// The root widget of the application.\n///\n/// This widget initializes and configures the application's core services, theming, localization, and navigation\n/// infrastructure.\nclass DpipApp extends StatefulWidget {\n  /// Creates a new [DpipApp] instance.\n  final String? initialShortcut;\n  const DpipApp({super.key, this.initialShortcut});\n\n  @override\n  State<DpipApp> createState() => _DpipAppState();\n}\n\nclass _DpipAppState extends State<DpipApp> with WidgetsBindingObserver {\n  bool _hasHandledInitialShortcut = false;\n\n  Future<void> _checkUpdate() async {\n    if (kDebugMode) return;\n\n    try {\n      if (Platform.isAndroid) {\n        final info = await InAppUpdate.checkForUpdate();\n\n        if (info.updateAvailability != UpdateAvailability.updateAvailable) return;\n\n        if (info.immediateUpdateAllowed) {\n          InAppUpdate.performImmediateUpdate();\n        } else if (info.flexibleUpdateAllowed) {\n          final updateResult = await InAppUpdate.startFlexibleUpdate();\n\n          if (updateResult != AppUpdateResult.success) return;\n\n          InAppUpdate.completeFlexibleUpdate();\n        }\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('_DpipState._checkUpdate', e, s);\n    }\n  }\n\n  Future<void> _checkNotificationPermission() async {\n    if (Platform.isAndroid) return;\n    await fcmReadyCompleter.future;\n    bool notificationAllowed = false;\n    final settings = await FirebaseMessaging.instance.getNotificationSettings();\n    notificationAllowed =\n        settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional;\n\n    if (!Preference.isFirstLaunch && !notificationAllowed) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        final ctx = router.routerDelegate.navigatorKey.currentContext;\n        if (ctx != null && mounted) {\n          WelcomePermissionsRoute().go(context);\n        }\n      });\n    }\n  }\n\n  void _tryHandleInitialShortcut() {\n    if (_hasHandledInitialShortcut) return;\n    if (widget.initialShortcut == null) return;\n\n    final ctx = router.routerDelegate.navigatorKey.currentContext;\n    if (ctx == null) return;\n\n    _hasHandledInitialShortcut = true;\n\n    switch (widget.initialShortcut) {\n      case 'monitor':\n        MapRoute(layers: MapLayer.monitor.name).push(context);\n        break;\n    }\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    GlobalProviders.data.onAppLifecycleStateChanged(state);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    _checkUpdate();\n    WidgetsBinding.instance.addObserver(this);\n    GlobalProviders.data.startFetching();\n    _checkNotificationPermission();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      Future.delayed(const Duration(milliseconds: 500), () {\n        handlePendingNotificationNavigation(context);\n      });\n      _tryHandleInitialShortcut();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DynamicColorBuilder(\n      builder: (lightDynamic, darkDynamic) {\n        return Consumer<SettingsUserInterfaceModel>(\n          builder: (context, model, child) {\n            final switchTheme = SwitchThemeData(\n              thumbIcon: .resolveWith(\n                (states) =>\n                    states.contains(WidgetState.selected) ? Icon(Symbols.check_rounded) : null,\n              ),\n            );\n\n            ThemeData lightTheme = .new(\n              colorSchemeSeed: model.themeColor ?? lightDynamic?.primary,\n              brightness: .light,\n              snackBarTheme: const .new(behavior: .floating),\n              pageTransitionsTheme: kZoomPageTransitionsTheme,\n              switchTheme: switchTheme,\n              // TODO(kamiya4047): Opt-in to new Material 3 update, remove this after it becomes the default option\n              sliderTheme: const .new(year2023: false),\n              progressIndicatorTheme: const .new(year2023: false),\n            );\n            ThemeData darkTheme = .new(\n              colorSchemeSeed: model.themeColor ?? darkDynamic?.primary,\n              brightness: .dark,\n              snackBarTheme: const .new(behavior: .floating),\n              pageTransitionsTheme: kZoomPageTransitionsTheme,\n              switchTheme: switchTheme,\n              // TODO(kamiya4047): Opt-in to new Material 3 update, remove this after it becomes the default option\n              sliderTheme: const .new(year2023: false),\n              progressIndicatorTheme: const .new(year2023: false),\n            );\n\n            final fontTextTheme = switch (I18n.locale.toLanguageTag()) {\n              'zh-Hans' => GoogleFonts.notoSansScTextTheme,\n              'ja' => GoogleFonts.notoSansJpTextTheme,\n              'ko' => GoogleFonts.notoSansKrTextTheme,\n              'vi' => GoogleFonts.notoSansTextTheme,\n              'ru' => GoogleFonts.notoSansTextTheme,\n              _ => GoogleFonts.notoSansTcTextTheme,\n            };\n\n            lightTheme = lightTheme.copyWith(\n              textTheme: GoogleFonts.latoTextTheme(\n                fontTextTheme(lightTheme.textTheme),\n              ),\n            );\n            darkTheme = darkTheme.copyWith(\n              textTheme: GoogleFonts.latoTextTheme(\n                fontTextTheme(darkTheme.textTheme),\n              ),\n            );\n\n            return MaterialApp.router(\n              builder: (context, child) {\n                final mediaQueryData = MediaQuery.of(context);\n                final scale = mediaQueryData.textScaler.clamp(\n                  minScaleFactor: 0.5,\n                  maxScaleFactor: 1.2,\n                );\n                return MediaQuery(\n                  data: mediaQueryData.copyWith(textScaler: scale),\n                  child: child!,\n                );\n              },\n              title: 'DPIP',\n              theme: lightTheme,\n              darkTheme: darkTheme,\n              themeMode: model.themeMode,\n              localizationsDelegates: I18n.localizationsDelegates,\n              supportedLocales: I18n.supportedLocales,\n              locale: I18n.locale,\n              routerConfig: router,\n            );\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/map.dart",
    "content": "import 'package:dpip/app_old/page/map/typhoon/typhoon.dart';\nimport 'package:dpip/app_old/page/map/weather/humidity.dart';\nimport 'package:dpip/app_old/page/map/weather/pressure.dart';\nimport 'package:dpip/widgets/list/tile_group_header.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass MapPage extends StatefulWidget {\n  const MapPage({super.key});\n\n  @override\n  State<MapPage> createState() => _MapPageState();\n}\n\nclass _MapPageState extends State<MapPage> {\n  final controller = PageController();\n  int currentIndex = 0;\n\n  late final destinations = [\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.humidity_percentage_rounded),\n      selectedIcon: Icon(Symbols.humidity_percentage_rounded, fill: 1),\n      label: Text('濕度'),\n    ),\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.blood_pressure_rounded),\n      selectedIcon: Icon(Symbols.blood_pressure_rounded, fill: 1),\n      label: Text('氣壓'),\n    ),\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.cyclone_rounded),\n      selectedIcon: Icon(Symbols.cyclone_rounded, fill: 1),\n      label: Text('颱風'),\n    ),\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.tsunami),\n      selectedIcon: Icon(Symbols.tsunami, fill: 1),\n      label: Text('海嘯資訊'),\n    ),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: destinations[currentIndex].label),\n      drawer: NavigationDrawer(\n        selectedIndex: currentIndex,\n        children: [\n          const ListTileGroupHeader(title: '地圖列表'),\n          ...destinations,\n        ],\n        onDestinationSelected: (value) {\n          setState(() => currentIndex = value);\n          controller.jumpToPage(value);\n          Navigator.pop(context);\n        },\n      ),\n      body: PageView(\n        controller: controller,\n        physics: const NeverScrollableScrollPhysics(),\n        children: const [HumidityMap(), PressureMap(), TyphoonMap()],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/meteor.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/meteor_station.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:fl_chart/fl_chart.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\ntypedef StationIdUpdateCallback = void Function(String?);\n\nclass AdvancedWeatherChart extends StatefulWidget {\n  final String stationId;\n  final VoidCallback onClose;\n  final String? type;\n\n  const AdvancedWeatherChart({\n    super.key,\n    required this.stationId,\n    required this.onClose,\n    this.type = 'temperature',\n  });\n\n  @override\n  State<AdvancedWeatherChart> createState() => _AdvancedWeatherChartState();\n\n  static StationIdUpdateCallback? _activeCallback;\n\n  static void setActiveCallback(StationIdUpdateCallback callback) {\n    _activeCallback = callback;\n  }\n\n  static void clearActiveCallback() {\n    _activeCallback = null;\n  }\n\n  static void updateStationId(String? getstationId) {\n    _activeCallback?.call(getstationId);\n  }\n}\n\nclass _AdvancedWeatherChartState extends State<AdvancedWeatherChart> {\n  String? selectedDataType;\n  int touchedIndex = -1;\n  bool isLoading = true;\n  Map<String, List<double>> weatherData = {};\n  List<double> windDirection = [];\n  MeteorStation? data;\n  String? stationId;\n\n  @override\n  void initState() {\n    super.initState();\n    stationId = widget.stationId;\n    _fetchWeatherData();\n    selectedDataType = widget.type;\n    AdvancedWeatherChart.setActiveCallback(_handleStationIdUpdate);\n  }\n\n  @override\n  void dispose() {\n    AdvancedWeatherChart.clearActiveCallback();\n    super.dispose();\n  }\n\n  void _handleStationIdUpdate(String? getstationId) {\n    if (mounted) {\n      setState(() {\n        stationId = getstationId;\n        isLoading = true;\n      });\n      _fetchWeatherData();\n    }\n  }\n\n  Future<void> _fetchWeatherData() async {\n    data = await ExpTech().getMeteorStation(stationId!);\n    setState(() {\n      windDirection = data!.windDirection.reversed.toList();\n      weatherData = {\n        'temperature': data!.temperature.reversed.toList(),\n        'wind_speed': data!.windSpeed.reversed.toList(),\n        'precipitation': data!.precipitation.reversed.toList(),\n        'humidity': data!.humidity.reversed.toList(),\n        'pressure': data!.pressure.reversed.toList(),\n        'time': data!.time.reversed.toList().map((item) => double.tryParse(item) ?? 0).toList(),\n      };\n      isLoading = false;\n    });\n  }\n\n  Map<String, String> get dataTypeToChineseMap {\n    return {\n      'temperature': '氣溫',\n      'wind_speed': '風向/風速',\n      'precipitation': '降水',\n      'humidity': '濕度',\n      'pressure': '氣壓',\n    };\n  }\n\n  final Map<String, String> units = {\n    'temperature': '°C',\n    'wind_speed': 'm/s',\n    'precipitation': 'mm',\n    'humidity': '%',\n    'pressure': 'hPa',\n  };\n\n  List<Color> getDataTypeColor(String dataType) {\n    switch (dataType) {\n      case 'temperature':\n        return [Colors.deepOrangeAccent, Colors.orangeAccent];\n      case 'wind_speed':\n        return [Colors.green, Colors.blue];\n      case 'precipitation':\n        return [Colors.blue, Colors.blue];\n      case 'humidity':\n        return [Colors.blueAccent, Colors.greenAccent];\n      case 'pressure':\n        return [Colors.purple, Colors.purple];\n      default:\n        return [Colors.grey, Colors.grey];\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        AppBar(\n          leading: IconButton(\n            icon: const Icon(Icons.arrow_back),\n            onPressed: widget.onClose,\n            tooltip: 'Back',\n          ),\n          automaticallyImplyLeading: false,\n          title: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(\n                '${data?.station.county ?? \"\"}${data?.station.name ?? \"\"}',\n                style: context.theme.textTheme.titleLarge?.copyWith(\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              Text(\n                stationId!,\n                style: context.theme.textTheme.bodyMedium?.copyWith(\n                  color: context.colors.onSurface.withValues(alpha: 0.6),\n                ),\n              ),\n            ],\n          ),\n          actions: [_buildDataTypeSelector(), const SizedBox(width: 16)],\n        ),\n        if (isLoading)\n          Padding(\n            padding: const EdgeInsets.all(16),\n            child: Center(\n              child: CircularProgressIndicator(\n                valueColor: AlwaysStoppedAnimation<Color>(\n                  context.colors.primary,\n                ),\n              ),\n            ),\n          )\n        else\n          Flexible(\n            child: SingleChildScrollView(\n              padding: const EdgeInsets.all(16),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  _buildHeader(),\n                  const SizedBox(height: 16),\n                  _buildChart(),\n                ],\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget _buildHeader() {\n    final String displayValue = touchedIndex != -1\n        ? '${DateFormat('MM/dd HH時').format(DateTime.fromMillisecondsSinceEpoch(weatherData['time']![touchedIndex].toInt()))}   ${weatherData[selectedDataType]![touchedIndex]}${units[selectedDataType]}'\n        : '平均  ${_calculate24HourAverage()}${units[selectedDataType]}';\n\n    return Card(\n      elevation: 4,\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  '24小時${dataTypeToChineseMap[selectedDataType]}趨勢',\n                  style: context.theme.textTheme.titleMedium,\n                ),\n                const SizedBox(height: 8),\n                Text(\n                  displayValue,\n                  style: context.theme.textTheme.titleSmall?.copyWith(\n                    color: getDataTypeColor(selectedDataType!)[0],\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n              ],\n            ),\n            if (selectedDataType == 'wind_speed' &&\n                touchedIndex != -1 &&\n                weatherData[selectedDataType]![touchedIndex] != 0)\n              Transform.rotate(\n                angle: (windDirection[touchedIndex] + 180) % 360 * 3.14159 / 180,\n                child: Icon(\n                  Icons.arrow_upward,\n                  color: getDataTypeColor(selectedDataType!)[0],\n                  size: 48,\n                ),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  String _calculate24HourAverage() {\n    final List<double> validData = weatherData[selectedDataType]!\n        .where((value) => value != -99)\n        .toList();\n    if (validData.isEmpty) return 'N/A';\n    final double sum = validData.reduce((a, b) => a + b);\n    return (sum / validData.length).toStringAsFixed(1);\n  }\n\n  Widget _buildChart() {\n    return Card(\n      elevation: 4,\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: Column(\n          children: [\n            const SizedBox(height: 8),\n            AspectRatio(\n              aspectRatio: 16 / 9,\n              child: selectedDataType == 'precipitation' ? _buildBarChart() : _buildLineChart(),\n            ),\n            const SizedBox(height: 8),\n            _buildLegend(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLineChart() {\n    final List<Color> lineColor = getDataTypeColor(selectedDataType!);\n    final List<FlSpot> spots = [];\n    double minY = double.infinity;\n    double maxY = double.negativeInfinity;\n\n    final bool invalid = weatherData[selectedDataType]?.every((value) => value == -99) ?? true;\n\n    if (invalid) {\n      return const Center(child: Text('沒有有效資料可顯示'));\n    }\n\n    for (int i = 0; i < weatherData[selectedDataType]!.length; i++) {\n      if (weatherData[selectedDataType]![i] == -99) {\n        spots.add(FlSpot.nullSpot);\n      } else {\n        final double value = weatherData[selectedDataType]![i];\n        spots.add(FlSpot(i.toDouble(), value));\n        minY = min(minY, value);\n        maxY = max(maxY, value);\n      }\n    }\n\n    double interval;\n    double startY;\n    double endY;\n\n    switch (selectedDataType) {\n      case 'temperature':\n        interval = 3;\n        startY = (minY / interval).floor() * interval;\n        endY = (maxY / interval).ceil() * interval;\n      case 'wind_speed':\n        interval = 1;\n        startY = minY.floor().toDouble();\n        endY = maxY.ceil().toDouble();\n      case 'humidity':\n        interval = 20;\n        startY = 0;\n        endY = 100;\n      case 'pressure':\n        interval = 15;\n        startY = (minY / interval).floor() * interval;\n        endY = (maxY / interval).ceil() * interval;\n      default:\n        interval = 1;\n        startY = minY.floor().toDouble();\n        endY = maxY.ceil().toDouble();\n    }\n\n    return LineChart(\n      LineChartData(\n        gridData: const FlGridData(show: false),\n        titlesData: FlTitlesData(\n          bottomTitles: AxisTitles(\n            sideTitles: SideTitles(\n              showTitles: true,\n              reservedSize: 30,\n              getTitlesWidget: (value, meta) {\n                final int index = value.toInt();\n                if (index >= 0 && index < weatherData['time']!.length) {\n                  final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(\n                    weatherData['time']![index].toInt(),\n                  );\n                  return Text(\n                    DateFormat('HH時').format(dateTime),\n                    style: const TextStyle(fontSize: 10),\n                  );\n                } else {\n                  return const Text('');\n                }\n              },\n            ),\n          ),\n          leftTitles: AxisTitles(\n            sideTitles: SideTitles(\n              showTitles: true,\n              reservedSize: 40,\n              interval: interval,\n              getTitlesWidget: (value, meta) {\n                if (value >= startY && value <= endY && (value % interval).abs() < 0.001) {\n                  return Text(\n                    value.toInt().toString(),\n                    style: const TextStyle(fontSize: 10),\n                  );\n                } else {\n                  return const Text('');\n                }\n              },\n            ),\n          ),\n          topTitles: const AxisTitles(),\n          rightTitles: const AxisTitles(),\n        ),\n        borderData: FlBorderData(show: true),\n        minY: startY,\n        maxY: endY,\n        lineBarsData: [\n          LineChartBarData(\n            spots: spots,\n            isCurved: true,\n            color: lineColor[0],\n            barWidth: 3,\n            isStrokeCapRound: true,\n            dotData: const FlDotData(show: false),\n            belowBarData: BarAreaData(\n              show: true,\n              color: lineColor[0].withValues(alpha: 0.3),\n              gradient: LinearGradient(\n                colors: [\n                  lineColor[0].withValues(alpha: 0.8),\n                  lineColor[1].withValues(alpha: 0.1),\n                ],\n                begin: Alignment.topCenter,\n                end: Alignment.bottomCenter,\n              ),\n            ),\n          ),\n        ],\n        lineTouchData: LineTouchData(\n          touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) {\n            setState(() {\n              if (event is FlPanEndEvent || event is FlTapUpEvent || event is FlLongPressEnd) {\n                touchedIndex = -1;\n              } else if (touchResponse?.lineBarSpots != null &&\n                  touchResponse!.lineBarSpots!.isNotEmpty) {\n                touchedIndex = touchResponse.lineBarSpots![0].x.toInt();\n              }\n            });\n          },\n          touchTooltipData: LineTouchTooltipData(\n            getTooltipItems: (List<LineBarSpot> touchedBarSpots) {\n              return List.filled(touchedBarSpots.length, null);\n            },\n          ),\n          getTouchedSpotIndicator: (LineChartBarData barData, List<int> spotIndexes) {\n            return spotIndexes.map((spotIndex) {\n              return TouchedSpotIndicatorData(\n                const FlLine(color: Colors.white, dashArray: [5, 5]),\n                FlDotData(\n                  getDotPainter: (spot, percent, barData, index) {\n                    return FlDotCirclePainter(\n                      radius: 3,\n                      color: Colors.white,\n                      strokeWidth: 2,\n                      strokeColor: Colors.grey,\n                    );\n                  },\n                ),\n              );\n            }).toList();\n          },\n        ),\n        extraLinesData: ExtraLinesData(\n          horizontalLines: [\n            HorizontalLine(\n              y: double.parse(_calculate24HourAverage()),\n              color: Colors.grey,\n              strokeWidth: 1,\n              dashArray: [5, 5],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildBarChart() {\n    final Color barColor = getDataTypeColor(selectedDataType!)[0];\n    final Color abnormalColor = Colors.red.withValues(alpha: 0.3);\n\n    final bool invalid = weatherData[selectedDataType]?.every((value) => value == -99) ?? true;\n\n    if (invalid) {\n      return const Center(child: Text('沒有有效資料可顯示'));\n    }\n\n    final double maxRainfall = weatherData[selectedDataType]!\n        .where((value) => value != -99)\n        .fold(0, (max, value) => value > max ? value : max);\n\n    final double interval = _calculateDynamicInterval(maxRainfall);\n\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        return CustomPaint(\n          painter: BackgroundPainter(\n            data: weatherData[selectedDataType]!,\n            abnormalColor: abnormalColor,\n            chartAreaSize: Size(constraints.maxWidth, constraints.maxHeight),\n          ),\n          child: BarChart(\n            BarChartData(\n              gridData: const FlGridData(show: false),\n              titlesData: FlTitlesData(\n                bottomTitles: AxisTitles(\n                  sideTitles: SideTitles(\n                    showTitles: true,\n                    reservedSize: 30,\n                    getTitlesWidget: (value, meta) {\n                      final int index = value.toInt();\n                      if (index % 3 == 0 && index >= 0 && index < weatherData['time']!.length) {\n                        final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(\n                          weatherData['time']![index].toInt(),\n                        );\n                        return SideTitleWidget(\n                          meta: meta,\n                          child: Text(\n                            DateFormat('HH時').format(dateTime),\n                            style: const TextStyle(fontSize: 10),\n                          ),\n                        );\n                      } else {\n                        return SideTitleWidget(\n                          meta: meta,\n                          child: const Text(''),\n                        );\n                      }\n                    },\n                  ),\n                ),\n                leftTitles: AxisTitles(\n                  sideTitles: SideTitles(\n                    showTitles: true,\n                    reservedSize: 40,\n                    interval: interval,\n                    getTitlesWidget: (value, meta) {\n                      if (value % interval < 0.001) {\n                        return Text(\n                          value.toInt().toString(),\n                          style: const TextStyle(fontSize: 10),\n                        );\n                      } else {\n                        return const Text('');\n                      }\n                    },\n                  ),\n                ),\n                topTitles: const AxisTitles(),\n                rightTitles: const AxisTitles(),\n              ),\n              borderData: FlBorderData(show: true),\n              barGroups: weatherData[selectedDataType]!\n                  .asMap()\n                  .entries\n                  .map(\n                    (entry) => BarChartGroupData(\n                      x: entry.key,\n                      barRods: [\n                        BarChartRodData(\n                          toY: entry.value == -99 ? 0 : entry.value,\n                          color: touchedIndex != -1 && touchedIndex != entry.key\n                              ? Colors.grey\n                              : barColor,\n                          width: 3,\n                        ),\n                      ],\n                    ),\n                  )\n                  .toList(),\n              barTouchData: BarTouchData(\n                enabled: true,\n                touchTooltipData: BarTouchTooltipData(\n                  getTooltipItem: (group, groupIndex, rod, rodIndex) => null,\n                ),\n                touchCallback: (FlTouchEvent event, BarTouchResponse? touchResponse) {\n                  setState(() {\n                    if (event is FlPanEndEvent ||\n                        event is FlTapUpEvent ||\n                        event is FlLongPressEnd) {\n                      touchedIndex = -1;\n                    } else if (touchResponse?.spot != null) {\n                      touchedIndex = touchResponse!.spot!.touchedBarGroupIndex;\n                    }\n                  });\n                },\n              ),\n              maxY: (maxRainfall / interval).ceil() * interval,\n              minY: 0,\n              backgroundColor: Colors.transparent,\n              extraLinesData: ExtraLinesData(\n                horizontalLines: [\n                  HorizontalLine(\n                    y: double.parse(_calculate24HourAverage()),\n                    color: Colors.grey,\n                    strokeWidth: 1,\n                    dashArray: [5, 5],\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  double _calculateDynamicInterval(double maxValue) {\n    if (maxValue <= 5) return 1;\n    if (maxValue <= 10) return 2;\n    if (maxValue <= 50) return 5;\n    if (maxValue <= 100) return 10;\n    return 20;\n  }\n\n  Widget _buildDataTypeSelector() {\n    final tempItems = <DropdownMenuItem<String>>[];\n\n    for (final value in weatherData.keys) {\n      final label = dataTypeToChineseMap[value];\n      if (label != null) {\n        tempItems.add(\n          DropdownMenuItem<String>(\n            value: value,\n            child: Text(\n              label,\n              style: TextStyle(\n                color: context.colors.onSecondaryContainer,\n                fontSize: 14,\n              ),\n            ),\n          ),\n        );\n      }\n    }\n    return Container(\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(10),\n        color: context.colors.secondaryContainer,\n      ),\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n      child: DropdownButtonHideUnderline(\n        child: DropdownButton<String>(\n          value: selectedDataType,\n          onChanged: (String? newValue) {\n            if (newValue != null) {\n              setState(() {\n                selectedDataType = newValue;\n                touchedIndex = -1;\n              });\n            }\n          },\n          items: tempItems,\n          icon: Icon(\n            Icons.arrow_drop_down,\n            color: context.colors.onSecondaryContainer,\n            size: 20,\n          ),\n          dropdownColor: context.colors.secondaryContainer,\n          borderRadius: BorderRadius.circular(16),\n          elevation: 2,\n          isDense: true,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLegend() {\n    return Row(\n      children: [\n        Row(\n          children: [\n            Container(\n              width: 20,\n              height: 3,\n              color: getDataTypeColor(selectedDataType!)[0],\n            ),\n            const SizedBox(width: 8),\n            Text(dataTypeToChineseMap[selectedDataType]!),\n          ],\n        ),\n        const SizedBox(width: 15),\n        Row(\n          children: [\n            Container(\n              width: 20,\n              height: 1,\n              decoration: const BoxDecoration(\n                border: Border(bottom: BorderSide(color: Colors.grey)),\n              ),\n            ),\n            const SizedBox(width: 8),\n            const Text('平均'),\n          ],\n        ),\n      ],\n    );\n  }\n}\n\nclass BackgroundPainter extends CustomPainter {\n  final List<double> data;\n  final Color abnormalColor;\n  final Size chartAreaSize;\n\n  BackgroundPainter({\n    required this.data,\n    required this.abnormalColor,\n    required this.chartAreaSize,\n  });\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final paint = Paint()\n      ..color = abnormalColor\n      ..style = PaintingStyle.fill;\n\n    const double leftPadding = 55;\n    const double bottomPadding = 30;\n    const double topPadding = 0;\n    const double rightPadding = 10;\n\n    final double chartWidth = chartAreaSize.width - leftPadding - rightPadding;\n    final double chartHeight = chartAreaSize.height - bottomPadding - topPadding;\n\n    final barWidth = chartWidth / data.length;\n\n    for (int i = 0; i < data.length; i++) {\n      if (data[i] == -99) {\n        final rect = Rect.fromLTWH(\n          leftPadding + i * barWidth,\n          topPadding,\n          barWidth,\n          chartHeight,\n        );\n        canvas.drawRect(rect, paint);\n      }\n    }\n  }\n\n  @override\n  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;\n}\n"
  },
  {
    "path": "lib/app_old/page/map/tsunami/tsunami_estimate_list.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:intl/intl.dart';\nimport 'package:timezone/timezone.dart' as tz;\n\nclass TsunamiEstimateList extends StatelessWidget {\n  final List tsunamiList;\n\n  const TsunamiEstimateList({super.key, required this.tsunamiList});\n\n  String convertTimestamp(int timestamp) {\n    final location = tz.getLocation('Asia/Taipei');\n    final DateTime dateTime = tz.TZDateTime.fromMillisecondsSinceEpoch(\n      location,\n      timestamp,\n    );\n\n    final DateFormat formatter = DateFormat('d日HH:mm');\n    final String formattedDate = formatter.format(dateTime);\n    return formattedDate;\n  }\n\n  String heightToString(height) {\n    if (height == 3) {\n      return '>3m';\n    } else if (height == 2) {\n      return '1~3m';\n    } else if (height == 1) {\n      return '0.3~1m';\n    } else {\n      return '<0.3m';\n    }\n  }\n\n  Color heightToColor(height) {\n    if (height == 3) {\n      return const Color(0xFFE543FF);\n    } else if (height == 2) {\n      return const Color(0xFFC90000);\n    } else if (height == 1) {\n      return const Color(0xFFFFC900);\n    } else {\n      return const Color(0xFF00AAFF);\n    }\n  }\n\n  Color heightToTextColor(height) {\n    if (height == 1) {\n      return const Color(0xFF202020);\n    } else {\n      return const Color(0xFFFFFFFF);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: tsunamiList.map((item) {\n        return Column(\n          children: [\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                Text(\n                  item.area,\n                  style: const TextStyle(fontSize: 18, letterSpacing: 2),\n                ),\n                Row(\n                  children: [\n                    Text(\n                      convertTimestamp(item.arrivalTime),\n                      style: const TextStyle(fontSize: 12),\n                    ),\n                    const SizedBox(width: 10),\n                    Container(\n                      decoration: BoxDecoration(\n                        color: heightToColor(item.waveHeight),\n                        borderRadius: BorderRadius.circular(8),\n                      ),\n                      width: 75,\n                      child: Center(\n                        child: Text(\n                          heightToString(item.waveHeight),\n                          style: TextStyle(\n                            color: heightToTextColor(item.waveHeight),\n                            fontWeight: FontWeight.bold,\n                            fontSize: 18,\n                            letterSpacing: 1,\n                          ),\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ],\n            ),\n            const SizedBox(height: 4),\n          ],\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/tsunami/tsunami_observed_list.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:intl/intl.dart';\nimport 'package:timezone/timezone.dart' as tz;\n\nclass TsunamiObservedList extends StatelessWidget {\n  final List tsunamiList;\n\n  const TsunamiObservedList({super.key, required this.tsunamiList});\n\n  String convertTimestamp(int timestamp) {\n    final location = tz.getLocation('Asia/Taipei');\n    final DateTime dateTime = tz.TZDateTime.fromMillisecondsSinceEpoch(\n      location,\n      timestamp,\n    );\n\n    final DateFormat formatter = DateFormat('dd日HH:mm');\n    final String formattedDate = formatter.format(dateTime);\n    return formattedDate;\n  }\n\n  Color heightToColor(height) {\n    if (height >= 300) {\n      return const Color(0xFFE543FF);\n    } else if (height >= 100) {\n      return const Color(0xFFC90000);\n    } else if (height >= 30) {\n      return const Color(0xFFFFC900);\n    } else {\n      return const Color(0xFF00AAFF);\n    }\n  }\n\n  Color heightToTextColor(height) {\n    if (height >= 100) {\n      return const Color(0xFFFFFFFF);\n    } else if (height >= 30) {\n      return const Color(0xFF202020);\n    } else {\n      return const Color(0xFFFFFFFF);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: tsunamiList.map((item) {\n        return Column(\n          children: [\n            Row(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                Text(\n                  item.name,\n                  style: const TextStyle(fontSize: 18, letterSpacing: 2),\n                ),\n                Row(\n                  children: [\n                    Text(\n                      convertTimestamp(item.arrivalTime),\n                      style: const TextStyle(fontSize: 12),\n                    ),\n                    const SizedBox(width: 10),\n                    Container(\n                      decoration: BoxDecoration(\n                        color: heightToColor(item.waveHeight),\n                        borderRadius: BorderRadius.circular(8),\n                      ),\n                      width: 95,\n                      child: Center(\n                        child: Text(\n                          '${item.waveHeight}cm',\n                          style: TextStyle(\n                            color: heightToTextColor(item.waveHeight),\n                            fontWeight: FontWeight.bold,\n                            fontSize: 18,\n                            letterSpacing: 1,\n                          ),\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ],\n            ),\n            const SizedBox(height: 4),\n          ],\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/typhoon/typhoon.dart",
    "content": "import 'package:dpip/api/exptech.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nclass TyphoonMap extends StatefulWidget {\n  const TyphoonMap({super.key});\n\n  @override\n  State<TyphoonMap> createState() => _TyphoonMapState();\n}\n\nclass _TyphoonMapState extends State<TyphoonMap> {\n  late MapLibreMapController _mapController;\n  List typhoonImagesList = [];\n  Map<String, dynamic> typhoonData = {};\n  List<String> typhoonList = [];\n  int selectedTyphoonId = -1;\n  List<String> sourceList = [];\n  List<String> layerList = [];\n  List<String> typhoon_name_list = [];\n  List<int> typhoon_id_list = [];\n  String selectedTimestamp = '';\n  double userLat = 0;\n  double userLon = 0;\n  bool isUserLocationValid = false;\n\n  void _initMap(MapLibreMapController controller) {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    try {\n      typhoonImagesList = await ExpTech().getTyphoonImagesList();\n      typhoonData = await ExpTech().getTyphoonGeojson();\n\n      if (GlobalProviders.location.auto) {\n        await updateLocationFromGPS();\n      }\n      userLat = Global.preference.getDouble('user-lat') ?? 0.0;\n      userLon = Global.preference.getDouble('user-lon') ?? 0.0;\n\n      isUserLocationValid = (userLon == 0 || userLat == 0) ? false : true;\n\n      if (isUserLocationValid) {\n        await _mapController.addSource(\n          'markers-geojson',\n          const GeojsonSourceProperties(\n            data: {'type': 'FeatureCollection', 'features': []},\n          ),\n        );\n        await _mapController.setGeoJsonSource('markers-geojson', {\n          'type': 'FeatureCollection',\n          'features': [\n            {\n              'type': 'Feature',\n              'properties': {},\n              'geometry': {\n                'coordinates': [userLon, userLat],\n                'type': 'Point',\n              },\n            },\n          ],\n        });\n      }\n\n      await _addUserLocationMarker();\n\n      _addTransparentLayerFromDataset();\n\n      _loadTyphoonLayers();\n\n      setState(() {});\n    } catch (e) {\n      TalkerManager.instance.error('加載颱風列表時出錯: $e');\n    }\n  }\n\n  Future<void> _addUserLocationMarker() async {\n    if (isUserLocationValid) {\n      await _mapController.removeLayer('markers');\n      await _mapController.addLayer(\n        'markers-geojson',\n        'markers',\n        const SymbolLayerProperties(\n          symbolZOrder: 'source',\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.5,\n            10,\n            1.5,\n          ],\n          iconImage: 'gps',\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n        ),\n      );\n    }\n  }\n\n  Future<void> _loadTyphoonLayers() async {\n    await _mapController.addSource(\n      'typhoon-geojson',\n      GeojsonSourceProperties(data: typhoonData),\n    );\n\n    await _mapController.addLayer(\n      'typhoon-geojson',\n      'typhoon-path',\n      const LineLayerProperties(\n        lineColor: [\n          'match',\n          [\n            'get',\n            'color',\n            ['properties'],\n          ],\n          0, '#1565C0', // 藍色\n          1, '#4CAF50', // 綠色\n          2, '#FFC107', // 黃色\n          3, '#FF5722', // 橙色\n          '#757575', // 默認灰色\n        ],\n        lineWidth: 2,\n      ),\n    );\n\n    await _mapController.addLayer(\n      'typhoon-geojson',\n      'typhoon-points',\n      const CircleLayerProperties(\n        circleRadius: 3,\n        circleColor: [\n          'match',\n          [\n            'get',\n            'color',\n            ['properties'],\n          ],\n          0,\n          '#1565C0',\n          1,\n          '#4CAF50',\n          2,\n          '#FFC107',\n          3,\n          '#FF5722',\n          '#757575',\n        ],\n        circleStrokeWidth: 2,\n        circleStrokeColor: '#FFFFFF',\n      ),\n      filter: [\n        'all',\n        [\n          '!=',\n          [\n            'get',\n            'forecast',\n            [\n              'get',\n              'type',\n              ['properties'],\n            ],\n          ],\n          true,\n        ],\n      ],\n    );\n\n    // 添加風圈圖層（只顯示第一個 type.forecast 為 true 的點）\n    await _mapController.addLayer(\n      'typhoon-geojson',\n      'typhoon-wind-circle',\n      const FillLayerProperties(\n        fillColor: 'rgba(255, 0, 0, 0.1)',\n        fillOutlineColor: 'rgba(255, 0, 0, 0.6)',\n      ),\n      filter: [\n        'all',\n        [\n          '==',\n          ['geometry-type'],\n          'Polygon',\n        ],\n        [\n          '==',\n          [\n            'get',\n            'type',\n            ['properties'],\n          ],\n          'wind-circle',\n        ],\n        [\n          '==',\n          [\n            'get',\n            'forecast',\n            [\n              'get',\n              'type',\n              ['properties'],\n            ],\n          ],\n          true,\n        ],\n        [\n          '==',\n          [\n            'get',\n            'tau',\n            [\n              'get',\n              'type',\n              ['properties'],\n            ],\n          ],\n          0,\n        ],\n      ],\n    );\n  }\n\n  void _addTransparentLayerFromDataset() {\n    final List<double> lonRange = [110, 150];\n    final List<double> latRange = [10, 32];\n\n    final bounds = LatLngBounds(\n      southwest: LatLng(latRange[0], lonRange[0]),\n      northeast: LatLng(latRange[1], lonRange[1]),\n    );\n\n    _mapController.addSource(\n      'radarOverlaySource',\n      ImageSourceProperties(\n        url: 'https://api-1.exptech.dev/api/v1/meteor/typhoon/images/${typhoonImagesList.last}',\n        coordinates: [\n          [bounds.southwest.longitude, bounds.northeast.latitude],\n          [bounds.northeast.longitude, bounds.northeast.latitude],\n          [bounds.northeast.longitude, bounds.southwest.latitude],\n          [bounds.southwest.longitude, bounds.southwest.latitude],\n        ],\n      ),\n    );\n\n    _mapController.addLayer(\n      'radarOverlaySource',\n      'radarOverlayLayer',\n      const RasterLayerProperties(rasterOpacity: 1),\n    );\n  }\n\n  @override\n  void dispose() {\n    _mapController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        DpipMap(\n          onMapCreated: _initMap,\n          onStyleLoadedCallback: _loadMap,\n          minMaxZoomPreference: const MinMaxZoomPreference(3, 12),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/weather/humidity.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/app_old/page/map/meteor.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/time_selector.dart';\nimport 'package:dpip/widgets/map/legend.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nclass HumidityData {\n  final double latitude;\n  final double longitude;\n  final int humidity;\n  final String stationName;\n  final String county;\n  final String town;\n  final String id;\n\n  HumidityData({\n    required this.latitude,\n    required this.longitude,\n    required this.humidity,\n    required this.stationName,\n    required this.county,\n    required this.town,\n    required this.id,\n  });\n}\n\nclass HumidityMap extends StatefulWidget {\n  const HumidityMap({super.key});\n\n  @override\n  State<HumidityMap> createState() => _HumidityMapState();\n}\n\nclass _HumidityMapState extends State<HumidityMap> {\n  late MapLibreMapController _mapController;\n\n  List<String> weather_list = [];\n  double userLat = 0;\n  double userLon = 0;\n  bool isUserLocationValid = false;\n  bool _showLegend = false;\n  String? _selectedStationId;\n\n  List<HumidityData> humidityDataList = [];\n\n  Future<void> _initMap(MapLibreMapController controller) async {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    }\n    userLat = Global.preference.getDouble('user-lat') ?? 0.0;\n    userLon = Global.preference.getDouble('user-lon') ?? 0.0;\n\n    isUserLocationValid = (userLon == 0 || userLat == 0) ? false : true;\n\n    await _mapController.addSource(\n      'humidity-data',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n\n    weather_list = await ExpTech().getWeatherList();\n\n    final List<WeatherStation> weatherData = await ExpTech().getWeather(\n      weather_list.last,\n    );\n\n    humidityDataList = weatherData\n        .where((station) => station.data.air.relativeHumidity != -99)\n        .map(\n          (station) => HumidityData(\n            id: station.id,\n            latitude: station.station.lat,\n            longitude: station.station.lng,\n            humidity: station.data.air.relativeHumidity,\n            stationName: station.station.name,\n            county: station.station.county,\n            town: station.station.town,\n          ),\n        )\n        .toList();\n\n    await addHumidityCircles(humidityDataList);\n\n    if (isUserLocationValid) {\n      await _mapController.addSource(\n        'markers-geojson',\n        const GeojsonSourceProperties(\n          data: {'type': 'FeatureCollection', 'features': []},\n        ),\n      );\n      await _mapController.setGeoJsonSource('markers-geojson', {\n        'type': 'FeatureCollection',\n        'features': [\n          {\n            'type': 'Feature',\n            'properties': {},\n            'geometry': {\n              'coordinates': [userLon, userLat],\n              'type': 'Point',\n            },\n          },\n        ],\n      });\n      final cameraUpdate = CameraUpdate.newLatLngZoom(\n        LatLng(userLat, userLon),\n        8,\n      );\n      await _mapController.animateCamera(\n        cameraUpdate,\n        duration: const Duration(milliseconds: 1000),\n      );\n    }\n\n    await _addUserLocationMarker();\n\n    setState(() {});\n  }\n\n  Future<void> _addUserLocationMarker() async {\n    if (isUserLocationValid) {\n      await _mapController.removeLayer('markers');\n      await _mapController.addLayer(\n        'markers-geojson',\n        'markers',\n        const SymbolLayerProperties(\n          symbolZOrder: 'source',\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.5,\n            10,\n            1.5,\n          ],\n          iconImage: 'gps',\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n        ),\n      );\n    }\n  }\n\n  Future<void> addHumidityCircles(List<HumidityData> humidityDataList) async {\n    final features = humidityDataList\n        .map(\n          (data) => {\n            'type': 'Feature',\n            'properties': {'id': data.id, 'humidity': data.humidity},\n            'geometry': {\n              'type': 'Point',\n              'coordinates': [data.longitude, data.latitude],\n            },\n          },\n        )\n        .toList();\n\n    await _mapController.setGeoJsonSource('humidity-data', {\n      'type': 'FeatureCollection',\n      'features': features,\n    });\n\n    await _mapController.removeLayer('humidity-circles');\n    await _mapController.addLayer(\n      'humidity-data',\n      'humidity-circles',\n      const CircleLayerProperties(\n        circleRadius: [\n          Expressions.interpolate,\n          ['linear'],\n          [Expressions.zoom],\n          7,\n          5,\n          12,\n          15,\n        ],\n        circleColor: [\n          Expressions.interpolate,\n          ['linear'],\n          [Expressions.get, 'humidity'],\n          0,\n          '#ffb63d',\n          50,\n          '#ffffff',\n          100,\n          '#0000FF',\n        ],\n        circleOpacity: 0.7,\n        circleStrokeWidth: 0.2,\n        circleStrokeColor: '#000000',\n        circleStrokeOpacity: 0.7,\n      ),\n    );\n\n    _mapController.onFeatureTapped.add((\n      dynamic feature,\n      Point<double> point,\n      LatLng latLng,\n      String layerId,\n    ) async {\n      final features = await _mapController.queryRenderedFeatures(point, [\n        'humidity-circles',\n      ], null);\n\n      if (features.isNotEmpty) {\n        final stationId = features[0]['properties']['id'] as String;\n        if (_selectedStationId != null) AdvancedWeatherChart.updateStationId(stationId);\n        setState(() {\n          _selectedStationId = stationId;\n        });\n      } else {\n        setState(() {\n          _selectedStationId = null;\n        });\n      }\n    });\n\n    await _mapController.removeLayer('humidity-labels');\n    await _mapController.addSymbolLayer(\n      'humidity-data',\n      'humidity-labels',\n      const SymbolLayerProperties(\n        textField: ['get', 'humidity'],\n        textSize: 12,\n        textColor: '#ffffff',\n        textHaloColor: '#000000',\n        textHaloWidth: 1,\n        textFont: ['Noto Sans TC Bold'],\n        textOffset: [\n          Expressions.literal,\n          [0, 2],\n        ],\n      ),\n      minzoom: 9,\n    );\n  }\n\n  void _toggleLegend() {\n    setState(() {\n      _showLegend = !_showLegend;\n    });\n  }\n\n  Widget _buildLegend() {\n    return MapLegend(\n      children: [\n        _buildColorBar(),\n        const SizedBox(height: 8),\n        _buildColorBarLabels(),\n        const SizedBox(height: 12),\n        Text('單位：相對濕度 (%)', style: context.theme.textTheme.labelMedium),\n      ],\n    );\n  }\n\n  Widget _buildColorBar() {\n    return Container(\n      height: 20,\n      width: 300,\n      decoration: const BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            Color(0xFFFFB63D), // 0% humidity\n            Color(0xFFFFFFFF), // 50% humidity\n            Color(0xFF0000FF), // 100% humidity\n          ],\n          stops: [0.0, 0.5, 1.0],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildColorBarLabels() {\n    final labels = ['0%', '25%', '50%', '75%', '100%'];\n    return SizedBox(\n      width: 300,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: labels.map((label) => Text(label, style: const TextStyle(fontSize: 12))).toList(),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _mapController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        DpipMap(\n          onMapCreated: _initMap,\n          onStyleLoadedCallback: _loadMap,\n          minMaxZoomPreference: const MinMaxZoomPreference(3, 12),\n        ),\n        Positioned(\n          left: 4,\n          bottom: 4,\n          child: Material(\n            color: context.colors.secondary,\n            elevation: 4.0,\n            shape: const CircleBorder(),\n            clipBehavior: Clip.antiAlias,\n            child: InkWell(\n              onTap: _toggleLegend,\n              child: Tooltip(\n                message: '圖例',\n                child: Container(\n                  width: 30,\n                  height: 30,\n                  alignment: Alignment.center,\n                  child: Icon(\n                    _showLegend ? Icons.close : Icons.info_outline,\n                    size: 20,\n                    color: context.colors.onSecondary,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n        if (_selectedStationId == null && weather_list.isNotEmpty)\n          Positioned(\n            left: 0,\n            right: 0,\n            bottom: 2,\n            child: TimeSelector(\n              timeList: weather_list,\n              onTimeExpanded: () {\n                _showLegend = false;\n                setState(() {});\n              },\n              onTimeSelected: (time) async {\n                final List<WeatherStation> weatherData = await ExpTech().getWeather(time);\n\n                humidityDataList = [];\n\n                humidityDataList = weatherData\n                    .where(\n                      (station) => station.data.air.relativeHumidity != -99,\n                    )\n                    .map(\n                      (station) => HumidityData(\n                        id: station.id,\n                        latitude: station.station.lat,\n                        longitude: station.station.lng,\n                        humidity: station.data.air.relativeHumidity,\n                        stationName: station.station.name,\n                        county: station.station.county,\n                        town: station.station.town,\n                      ),\n                    )\n                    .toList();\n\n                await addHumidityCircles(humidityDataList);\n                await _addUserLocationMarker();\n                setState(() {});\n              },\n            ),\n          ),\n        if (_showLegend) Positioned(left: 6, bottom: 50, child: _buildLegend()),\n        if (_selectedStationId != null)\n          DraggableScrollableSheet(\n            initialChildSize: 0.3,\n            minChildSize: 0.1,\n            snap: true,\n            snapSizes: const [0.1, 0.3, 0.7, 1],\n            builder: (BuildContext context, ScrollController scrollController) {\n              return Container(\n                decoration: BoxDecoration(\n                  color: context.theme.cardColor,\n                  borderRadius: const BorderRadius.vertical(\n                    top: Radius.circular(16),\n                  ),\n                  boxShadow: [\n                    BoxShadow(\n                      color: Colors.black.withValues(alpha: 0.1),\n                      blurRadius: 10,\n                      offset: const Offset(0, -5),\n                    ),\n                  ],\n                ),\n                child: SingleChildScrollView(\n                  controller: scrollController,\n                  child: Column(\n                    children: [\n                      Container(\n                        height: 4,\n                        width: 40,\n                        margin: const EdgeInsets.symmetric(vertical: 8),\n                        decoration: BoxDecoration(\n                          color: Colors.grey[300],\n                          borderRadius: BorderRadius.circular(2),\n                        ),\n                      ),\n                      AdvancedWeatherChart(\n                        type: 'humidity',\n                        stationId: _selectedStationId!,\n                        onClose: () {\n                          setState(() {\n                            _selectedStationId = null;\n                          });\n                        },\n                      ),\n                    ],\n                  ),\n                ),\n              );\n            },\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/map/weather/pressure.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/app_old/page/map/meteor.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/list/time_selector.dart';\nimport 'package:dpip/widgets/map/legend.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nclass PressureData {\n  final double latitude;\n  final double longitude;\n  final double pressure;\n  final String stationName;\n  final String county;\n  final String town;\n  final String id;\n\n  PressureData({\n    required this.latitude,\n    required this.longitude,\n    required this.pressure,\n    required this.stationName,\n    required this.county,\n    required this.town,\n    required this.id,\n  });\n}\n\nclass PressureMap extends StatefulWidget {\n  const PressureMap({super.key});\n\n  @override\n  State<PressureMap> createState() => _PressureMapState();\n}\n\nclass _PressureMapState extends State<PressureMap> {\n  late MapLibreMapController _mapController;\n\n  List<String> weather_list = [];\n  double userLat = 0;\n  double userLon = 0;\n  bool isUserLocationValid = false;\n  bool _showLegend = false;\n  String? _selectedStationId;\n\n  List<PressureData> pressureDataList = [];\n\n  Future<void> _initMap(MapLibreMapController controller) async {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    }\n    userLat = Global.preference.getDouble('user-lat') ?? 0.0;\n    userLon = Global.preference.getDouble('user-lon') ?? 0.0;\n\n    isUserLocationValid = (userLon == 0 || userLat == 0) ? false : true;\n\n    await _mapController.addSource(\n      'pressure-data',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n\n    weather_list = await ExpTech().getWeatherList();\n\n    final List<WeatherStation> weatherData = await ExpTech().getWeather(\n      weather_list.last,\n    );\n\n    pressureDataList = weatherData\n        .where((station) => station.data.air.pressure != -99)\n        .map(\n          (station) => PressureData(\n            id: station.id,\n            latitude: station.station.lat,\n            longitude: station.station.lng,\n            pressure: station.data.air.pressure,\n            stationName: station.station.name,\n            county: station.station.county,\n            town: station.station.town,\n          ),\n        )\n        .toList();\n\n    await addPressureCircles(pressureDataList);\n\n    if (isUserLocationValid) {\n      await _mapController.addSource(\n        'markers-geojson',\n        const GeojsonSourceProperties(\n          data: {'type': 'FeatureCollection', 'features': []},\n        ),\n      );\n      await _mapController.setGeoJsonSource('markers-geojson', {\n        'type': 'FeatureCollection',\n        'features': [\n          {\n            'type': 'Feature',\n            'properties': {},\n            'geometry': {\n              'coordinates': [userLon, userLat],\n              'type': 'Point',\n            },\n          },\n        ],\n      });\n      final cameraUpdate = CameraUpdate.newLatLngZoom(\n        LatLng(userLat, userLon),\n        8,\n      );\n      await _mapController.animateCamera(\n        cameraUpdate,\n        duration: const Duration(milliseconds: 1000),\n      );\n    }\n\n    await _addUserLocationMarker();\n\n    setState(() {});\n  }\n\n  Future<void> _addUserLocationMarker() async {\n    if (isUserLocationValid) {\n      await _mapController.removeLayer('markers');\n      await _mapController.addLayer(\n        'markers-geojson',\n        'markers',\n        const SymbolLayerProperties(\n          symbolZOrder: 'source',\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.5,\n            10,\n            1.5,\n          ],\n          iconImage: 'gps',\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n        ),\n      );\n    }\n  }\n\n  Future<void> addPressureCircles(List<PressureData> pressureDataList) async {\n    final features = pressureDataList\n        .map(\n          (data) => {\n            'type': 'Feature',\n            'properties': {'id': data.id, 'pressure': data.pressure},\n            'geometry': {\n              'type': 'Point',\n              'coordinates': [data.longitude, data.latitude],\n            },\n          },\n        )\n        .toList();\n\n    await _mapController.setGeoJsonSource('pressure-data', {\n      'type': 'FeatureCollection',\n      'features': features,\n    });\n\n    await _mapController.removeLayer('pressure-circles');\n    await _mapController.addLayer(\n      'pressure-data',\n      'pressure-circles',\n      const CircleLayerProperties(\n        circleRadius: [\n          Expressions.interpolate,\n          ['linear'],\n          [Expressions.zoom],\n          7,\n          5,\n          12,\n          15,\n        ],\n        circleColor: [\n          Expressions.interpolate,\n          ['linear'],\n          [Expressions.get, 'pressure'],\n          725,\n          '#77bfcc',\n          850,\n          '#82cb75',\n          975,\n          '#f7e78a',\n          1020,\n          '#ffffff',\n        ],\n        circleOpacity: 0.7,\n        circleStrokeWidth: 0.2,\n        circleStrokeColor: '#000000',\n        circleStrokeOpacity: 0.7,\n      ),\n    );\n\n    _mapController.onFeatureTapped.add((\n      dynamic feature,\n      Point<double> point,\n      LatLng latLng,\n      String layerId,\n    ) async {\n      final features = await _mapController.queryRenderedFeatures(point, [\n        'pressure-circles',\n      ], null);\n\n      if (features.isNotEmpty) {\n        final stationId = features[0]['properties']['id'] as String;\n        if (_selectedStationId != null) AdvancedWeatherChart.updateStationId(stationId);\n        setState(() {\n          _selectedStationId = stationId;\n        });\n      } else {\n        setState(() {\n          _selectedStationId = null;\n        });\n      }\n    });\n\n    await _mapController.removeLayer('pressure-labels');\n    await _mapController.addSymbolLayer(\n      'pressure-data',\n      'pressure-labels',\n      const SymbolLayerProperties(\n        textField: ['get', 'pressure'],\n        textSize: 12,\n        textColor: '#ffffff',\n        textHaloColor: '#000000',\n        textHaloWidth: 1,\n        textFont: ['Noto Sans TC Bold'],\n        textOffset: [\n          Expressions.literal,\n          [0, 2],\n        ],\n      ),\n      minzoom: 9,\n    );\n  }\n\n  void _toggleLegend() {\n    setState(() {\n      _showLegend = !_showLegend;\n    });\n  }\n\n  Widget _buildLegend() {\n    return MapLegend(\n      children: [\n        _buildColorBar(),\n        const SizedBox(height: 8),\n        _buildColorBarLabels(),\n        const SizedBox(height: 12),\n        Text('單位：百帕 (hPa)', style: context.theme.textTheme.labelMedium),\n      ],\n    );\n  }\n\n  Widget _buildColorBar() {\n    return Container(\n      height: 20,\n      width: 300,\n      decoration: const BoxDecoration(\n        borderRadius: BorderRadius.all(Radius.circular(4)),\n        gradient: LinearGradient(\n          colors: [\n            Color(0xFF77BFCC), // 725 hPa\n            Color(0xFF82CB75), // 850 hPa\n            Color(0xFFF7E78A), // 975 hPa\n            Color(0xFFFFFFFF), // 1020 hPa\n          ],\n          stops: [0.0, 0.4167, 0.8333, 1.0],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildColorBarLabels() {\n    final labels = ['725', '850', '975', '1020'];\n    return SizedBox(\n      width: 300,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: labels.map((label) => Text(label, style: const TextStyle(fontSize: 12))).toList(),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _mapController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        DpipMap(\n          onMapCreated: _initMap,\n          onStyleLoadedCallback: _loadMap,\n          minMaxZoomPreference: const MinMaxZoomPreference(3, 12),\n        ),\n        Positioned(\n          left: 4,\n          bottom: 4,\n          child: Material(\n            color: context.colors.secondary,\n            elevation: 4.0,\n            shape: const CircleBorder(),\n            clipBehavior: Clip.antiAlias,\n            child: InkWell(\n              onTap: _toggleLegend,\n              child: Tooltip(\n                message: '圖例',\n                child: Container(\n                  width: 30,\n                  height: 30,\n                  alignment: Alignment.center,\n                  child: Icon(\n                    _showLegend ? Icons.close : Icons.info_outline,\n                    size: 20,\n                    color: context.colors.onSecondary,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n        if (_selectedStationId == null && weather_list.isNotEmpty)\n          Positioned(\n            left: 0,\n            right: 0,\n            bottom: 2,\n            child: TimeSelector(\n              timeList: weather_list,\n              onTimeExpanded: () {\n                _showLegend = false;\n                setState(() {});\n              },\n              onTimeSelected: (time) async {\n                final List<WeatherStation> weatherData = await ExpTech().getWeather(time);\n\n                pressureDataList = [];\n\n                pressureDataList = weatherData\n                    .where((station) => station.data.air.pressure != -99)\n                    .map(\n                      (station) => PressureData(\n                        id: station.id,\n                        latitude: station.station.lat,\n                        longitude: station.station.lng,\n                        pressure: station.data.air.pressure,\n                        stationName: station.station.name,\n                        county: station.station.county,\n                        town: station.station.town,\n                      ),\n                    )\n                    .toList();\n\n                await addPressureCircles(pressureDataList);\n                await _addUserLocationMarker();\n                setState(() {});\n              },\n            ),\n          ),\n        if (_showLegend) Positioned(left: 6, bottom: 50, child: _buildLegend()),\n        if (_selectedStationId != null)\n          DraggableScrollableSheet(\n            initialChildSize: 0.3,\n            minChildSize: 0.1,\n            snap: true,\n            snapSizes: const [0.1, 0.3, 0.7, 1],\n            builder: (BuildContext context, ScrollController scrollController) {\n              return Container(\n                decoration: BoxDecoration(\n                  color: context.theme.cardColor,\n                  borderRadius: const BorderRadius.vertical(\n                    top: Radius.circular(16),\n                  ),\n                  boxShadow: [\n                    BoxShadow(\n                      color: Colors.black.withValues(alpha: 0.1),\n                      blurRadius: 10,\n                      offset: const Offset(0, -5),\n                    ),\n                  ],\n                ),\n                child: SingleChildScrollView(\n                  controller: scrollController,\n                  child: Column(\n                    children: [\n                      Container(\n                        height: 4,\n                        width: 40,\n                        margin: const EdgeInsets.symmetric(vertical: 8),\n                        decoration: BoxDecoration(\n                          color: Colors.grey[300],\n                          borderRadius: BorderRadius.circular(2),\n                        ),\n                      ),\n                      AdvancedWeatherChart(\n                        type: 'pressure',\n                        stationId: _selectedStationId!,\n                        onClose: () {\n                          setState(() {\n                            _selectedStationId = null;\n                          });\n                        },\n                      ),\n                    ],\n                  ),\n                ),\n              );\n            },\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/me/developer.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass DPIPInfoPage extends StatelessWidget {\n  const DPIPInfoPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('開發者想說的話'),\n        leading: IconButton(\n          icon: const Icon(Symbols.arrow_back),\n          onPressed: () => Navigator.pop(context),\n        ),\n      ),\n      body: Container(\n        decoration: BoxDecoration(\n          gradient: LinearGradient(\n            begin: Alignment.topCenter,\n            end: Alignment.bottomCenter,\n            colors: [\n              context.colors.primary.withValues(alpha: 0.05),\n              context.colors.surface,\n            ],\n          ),\n        ),\n        child: ListView(\n          padding: const EdgeInsets.all(16.0),\n          children: [\n            _buildHeroCard(context),\n            const SizedBox(height: 24),\n            _buildInfoCard(context, '簡介', [\n              '首先感謝所有下載這個軟體的使用者，整個開發團隊在此獻上最誠摯的謝意。',\n              'DPIP 是一個以整合所有防災資訊為目標的軟體，希望能成為民眾生活中不可或缺的一部分。儘管目前完成度不高且困難重重，但我們仍會持續朝這個目標前進。',\n              '在開發軟體時，我們投入了大量的金錢、時間與精力，在人事成本、設備費用、雲端服務、網路費用等項目上，花費超過50萬新台幣。為此，我們希望獲得使用者的支持，在不依賴其他第三方公司的前提下，繼續維持營運。',\n            ], Symbols.info_rounded),\n            _buildInfoCard(context, '營利模式', [\n              '為了維持 App 的開發，團隊內部進行了多次激烈討論，思考如何才能營利？我們試圖在眾多方案中，找出一個適合的營利模式。我們發現，大多數同類型軟體採用植入廣告的方式來達到營利目的，這使得我們一度考慮採用該方式作為營利的方法。',\n            ], Symbols.monetization_on_rounded),\n            _buildInfoCard(context, '營利真的太難了', [\n              '我們調查了一般民眾的付費意願，發現大部分人普遍防災意識不足，更不會花錢在這件事情上。後台的數據能側面證實這個說法，據統計，熱心贊助的民眾大約是整體使用者的10萬分之1，這使得植入廣告似乎成為了最好的解決方法。',\n            ], Symbols.trending_down_rounded),\n            _buildInfoCard(context, '為什麼不採用廣告？', [\n              '當災害發生時，大家一定不會想要看廣告吧？這是我們不植入廣告的第一個理由。防災導向的軟體，快速正確地傳遞防災資訊是首要任務。如果因為廣告而導致無法正確掌握防災資訊，這反而和我們的理念相違背。況且，災害發生時通常通訊品質不佳，還要額外浪費網路流量在載入廣告，這件事太令人沮喪了。',\n            ], Symbols.block_rounded),\n            _buildInfoCard(context, '對大眾收費？', [\n              '如果植入廣告行不通，那對大眾收費呢？變成付費軟體？',\n              '首先，作為防災軟體，我們希望盡可能地將防災資訊傳遞給越多人越好。而且，或許真正需要的人沒辦法再多出額外的經費承擔這項支出，我們希望幫助更多的人。其次，作為開發人員，我們希望軟體可以有很多人使用，收費會直接導致大家使用意願降低。',\n            ], Symbols.attach_money_rounded),\n            _buildInfoCard(context, '如何營利？', [\n              '總結上述，我們希望培養出對防災有興趣的人、重視防災的人，支持我們的軟體開發，一起往前發展。',\n              '2024/08/28   YuYu1015',\n              '©2024 ExpTech Studio Ltd.',\n            ], Symbols.lightbulb_rounded),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildHeroCard(BuildContext context) {\n    return Card(\n      elevation: 4,\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),\n      child: Padding(\n        padding: const EdgeInsets.all(24.0),\n        child: Column(\n          children: [\n            Image.asset(\n              'assets/DPIP.png', // 替換為實際的 DPIP logo 資源路徑\n              height: 100,\n              width: 100,\n            ),\n            const SizedBox(height: 16),\n            Text(\n              'DPIP 開發者的話',\n              style: context.theme.textTheme.headlineMedium?.copyWith(\n                fontWeight: FontWeight.bold,\n                color: context.colors.primary,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildInfoCard(\n    BuildContext context,\n    String title,\n    List<String> paragraphs,\n    IconData icon,\n  ) {\n    return Card(\n      elevation: 2,\n      margin: const EdgeInsets.only(bottom: 16),\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n      child: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                Icon(icon, color: context.colors.primary, size: 28),\n                const SizedBox(width: 12),\n                Text(\n                  title,\n                  style: context.theme.textTheme.titleLarge?.copyWith(\n                    fontWeight: FontWeight.bold,\n                    color: context.colors.primary,\n                  ),\n                ),\n              ],\n            ),\n            const Divider(height: 24),\n            ...paragraphs.map(\n              (paragraph) => Padding(\n                padding: const EdgeInsets.only(bottom: 8.0),\n                child: Text(\n                  paragraph,\n                  style: context.theme.textTheme.bodyLarge,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/me/me.dart",
    "content": "import 'package:dpip/app_old/page/me/developer.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/widgets/list/tile_group_header.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass MePage extends StatefulWidget {\n  const MePage({super.key});\n\n  @override\n  State<MePage> createState() => _MePageState();\n}\n\nclass _MePageState extends State<MePage> {\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        const ListTileGroupHeader(title: '關於'),\n        ListTile(\n          leading: const Icon(Symbols.forum_rounded),\n          title: const Text('開發者想說的話'),\n          onTap: () {\n            Navigator.push(\n              context,\n              MaterialPageRoute(builder: (context) => const DPIPInfoPage()),\n            );\n          },\n        ),\n\n        /**\n         * 打開歡迎頁面\n         */\n        ListTile(\n          leading: const Icon(Icons.visibility),\n          title: const Text('歡迎頁面'),\n          onTap: () => WelcomeRoute().push(context),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/more.dart",
    "content": "import 'package:dpip/app_old/page/more/ranking/ranking.dart';\nimport 'package:dpip/app_old/page/more/report_list/report_list.dart';\nimport 'package:dpip/widgets/list/tile_group_header.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass MorePage extends StatefulWidget {\n  const MorePage({super.key});\n\n  @override\n  State<MorePage> createState() => _MorePageState();\n}\n\nclass _MorePageState extends State<MorePage> {\n  final controller = PageController();\n  int currentIndex = 0;\n\n  late final destinations = [\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.summarize),\n      selectedIcon: Icon(Symbols.summarize, fill: 1),\n      label: Text('地震報告'),\n    ),\n    const NavigationDrawerDestination(\n      icon: Icon(Symbols.leaderboard_rounded),\n      selectedIcon: Icon(Symbols.leaderboard_rounded, fill: 1),\n      label: Text('排行榜'),\n    ),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      drawer: NavigationDrawer(\n        selectedIndex: currentIndex,\n        children: [\n          const ListTileGroupHeader(title: '更多功能列表'),\n          ...destinations,\n        ],\n        onDestinationSelected: (value) {\n          setState(() => currentIndex = value);\n          controller.jumpToPage(value);\n          Navigator.pop(context);\n        },\n      ),\n      body: PageView(\n        controller: controller,\n        physics: const NeverScrollableScrollPhysics(),\n        children: const [ReportListPage(), RankingPage()],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/ranking/ranking.dart",
    "content": "import 'package:dpip/app_old/page/more/ranking/tabs/precipitation.dart';\nimport 'package:dpip/app_old/page/more/ranking/tabs/temperature.dart';\nimport 'package:dpip/app_old/page/more/ranking/tabs/wind.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass RankingPage extends StatefulWidget {\n  const RankingPage({super.key});\n\n  @override\n  State<RankingPage> createState() => _RankingPageState();\n}\n\nclass _RankingPageState extends State<RankingPage> with TickerProviderStateMixin {\n  late final controller = TabController(length: 3, vsync: this);\n  final scroll = GlobalKey<NestedScrollViewState>();\n  bool showFAB = false;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      if (!mounted) return;\n      final innerController = scroll.currentState?.innerController;\n      if (innerController == null) return;\n      innerController.addListener(() {\n        if (controller.indexIsChanging) return;\n\n        if (innerController.offset == innerController.position.minScrollExtent) {\n          if (showFAB) setState(() => showFAB = false);\n        } else {\n          if (!showFAB) setState(() => showFAB = true);\n        }\n      });\n    });\n    controller.addListener(() {\n      if (!controller.indexIsChanging) return;\n      scroll.currentState?.outerController.animateTo(\n        0,\n        duration: Durations.long2,\n        curve: Easing.standard,\n      );\n      setState(() => showFAB = false);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return NestedScrollView(\n      key: scroll,\n      headerSliverBuilder: (context, innerBoxIsScrolled) {\n        return [\n          SliverAppBar(\n            pinned: true,\n            floating: true,\n            snap: true,\n            title: const Text('排行榜'),\n            bottom: TabBar(\n              controller: controller,\n              isScrollable: true,\n              tabs: const [\n                Tab(text: '降水'),\n                Tab(text: '氣溫'),\n                Tab(text: '風向/風速'),\n              ],\n            ),\n          ),\n        ];\n      },\n      body: Stack(\n        children: [\n          TabBarView(\n            controller: controller,\n            physics: const NeverScrollableScrollPhysics(),\n            children: const [\n              RankingPrecipitationTab(),\n              RankingTemperatureTab(),\n              RankingWindTab(),\n            ],\n          ),\n          Positioned(\n            bottom: 8,\n            right: 8,\n            child: AnimatedScale(\n              scale: showFAB ? 1 : 0,\n              duration: Durations.short4,\n              curve: Easing.standard,\n              child: FloatingActionButton.small(\n                child: const Icon(Symbols.vertical_align_top_rounded),\n                onPressed: () {\n                  scroll.currentState?.innerController.animateTo(\n                    0,\n                    duration: Durations.medium1,\n                    curve: Easing.standard,\n                  );\n                  scroll.currentState?.outerController.animateTo(\n                    0,\n                    duration: Durations.long2,\n                    curve: Easing.standard,\n                  );\n                },\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/ranking/tabs/precipitation.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/rain.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/intervals.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass RankingPrecipitationTab extends StatefulWidget {\n  const RankingPrecipitationTab({super.key});\n\n  @override\n  State<RankingPrecipitationTab> createState() => _RankingPrecipitationTabState();\n}\n\nclass _RankingPrecipitationTabState extends State<RankingPrecipitationTab> {\n  Intervals interval = Intervals.now;\n  String time = '';\n  Map<StationInfo, RainData> data = {};\n  List<(StationInfo, double)> ranked = [];\n\n  Future refresh() async {\n    final rainTimeList = await ExpTech().getRainList();\n    final rainData = await ExpTech().getRain(rainTimeList.last);\n\n    if (!mounted) return;\n\n    data = rainData.asMap().map((_, e) => MapEntry(e.station, e.data));\n    time = DateFormat(\n      'yyyy/MM/dd HH:mm:ss',\n    ).format(parseDateTime(rainTimeList.last));\n    rank();\n  }\n\n  void rank() {\n    setState(() {\n      ranked = data.entries\n          .map((e) {\n            double value;\n            switch (interval) {\n              case Intervals.now:\n                value = e.value.now;\n              case Intervals.tenMinutes:\n                value = e.value.tenMinutes;\n              case Intervals.oneHour:\n                value = e.value.oneHour;\n              case Intervals.threeHours:\n                value = e.value.threeHours;\n              case Intervals.sixHours:\n                value = e.value.sixHours;\n              case Intervals.twelveHours:\n                value = e.value.twelveHours;\n              case Intervals.twentyFourHours:\n                value = e.value.twentyFourHours;\n              case Intervals.twoDays:\n                value = e.value.twoDays;\n              case Intervals.threeDays:\n                value = e.value.threeDays;\n            }\n            return (e.key, value);\n          })\n          .where((e) => e.$2 > 0)\n          .sorted((a, b) => (b.$2 - a.$2).sign.toInt());\n    });\n  }\n\n  void setInterval(Intervals i) {\n    interval = i;\n    rank();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: refresh,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          SizedBox(\n            height: kToolbarHeight,\n            child: SingleChildScrollView(\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              scrollDirection: Axis.horizontal,\n              child: Wrap(\n                spacing: 8,\n                runAlignment: WrapAlignment.center,\n                crossAxisAlignment: WrapCrossAlignment.center,\n                children: [\n                  ChoiceChip(\n                    label: const Text('今日'),\n                    selected: interval == Intervals.now,\n                    onSelected: (value) => setInterval(Intervals.now),\n                  ),\n                  ChoiceChip(\n                    label: const Text('10 分鐘'),\n                    selected: interval == Intervals.tenMinutes,\n                    onSelected: (value) => setInterval(Intervals.tenMinutes),\n                  ),\n                  ChoiceChip(\n                    label: const Text('1 小時'),\n                    selected: interval == Intervals.oneHour,\n                    onSelected: (value) => setInterval(Intervals.oneHour),\n                  ),\n                  ChoiceChip(\n                    label: const Text('3 小時'),\n                    selected: interval == Intervals.threeHours,\n                    onSelected: (value) => setInterval(Intervals.threeHours),\n                  ),\n                  ChoiceChip(\n                    label: const Text('6 小時'),\n                    selected: interval == Intervals.sixHours,\n                    onSelected: (value) => setInterval(Intervals.sixHours),\n                  ),\n                  ChoiceChip(\n                    label: const Text('12 小時'),\n                    selected: interval == Intervals.twelveHours,\n                    onSelected: (value) => setInterval(Intervals.twelveHours),\n                  ),\n                  ChoiceChip(\n                    label: const Text('24 小時'),\n                    selected: interval == Intervals.twentyFourHours,\n                    onSelected: (value) => setInterval(Intervals.twentyFourHours),\n                  ),\n                  ChoiceChip(\n                    label: const Text('2 天'),\n                    selected: interval == Intervals.twoDays,\n                    onSelected: (value) => setInterval(Intervals.twoDays),\n                  ),\n                  ChoiceChip(\n                    label: const Text('3 天'),\n                    selected: interval == Intervals.threeDays,\n                    onSelected: (value) => setInterval(Intervals.threeDays),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n            child: Text(\n              '資料時間：$time\\n共 ${ranked.length} 觀測點',\n              style: TextStyle(color: context.colors.onSurfaceVariant),\n            ),\n          ),\n          Expanded(\n            child: ListView.builder(\n              padding: const EdgeInsets.symmetric(vertical: 4),\n              itemCount: ranked.isEmpty ? 1 : ranked.length,\n              itemBuilder: (context, index) {\n                if (ranked.isEmpty) {\n                  return const Padding(\n                    padding: EdgeInsets.only(top: 16),\n                    child: Center(child: CircularProgressIndicator()),\n                  );\n                }\n\n                final item = ranked[index];\n                final rank = index + 1;\n\n                final backgroundColor = index == 0\n                    ? context.theme.extendedColors.amberContainer\n                    : index == 1\n                    ? context.theme.extendedColors.greyContainer\n                    : index == 2\n                    ? context.theme.extendedColors.brownContainer\n                    : index < 10\n                    ? context.colors.surfaceContainerHigh\n                    : context.colors.surfaceContainer;\n\n                final foregroundColor = index == 0\n                    ? context.theme.extendedColors.onAmberContainer\n                    : index == 1\n                    ? context.colors.onSurface\n                    : index == 2\n                    ? context.theme.extendedColors.onBrownContainer\n                    : index < 10\n                    ? context.colors.onSurface\n                    : context.colors.onSurfaceVariant;\n\n                final iconColor = index == 0\n                    ? context.theme.extendedColors.amber\n                    : index == 1\n                    ? context.theme.extendedColors.grey\n                    : context.theme.extendedColors.brown;\n\n                final double fontSize = index == 0\n                    ? 20\n                    : index < 3\n                    ? 18\n                    : 16;\n\n                final double iconSize = index == 0\n                    ? 32\n                    : index == 1\n                    ? 28\n                    : 24;\n\n                final leading = index < 3\n                    ? Icon(\n                        index == 0 ? Symbols.trophy_rounded : Symbols.workspace_premium_rounded,\n                        color: iconColor,\n                        size: iconSize,\n                        fill: 1,\n                      )\n                    : Text(\n                        '$rank',\n                        style: TextStyle(\n                          color: foregroundColor,\n                          fontSize: fontSize,\n                        ),\n                      );\n\n                final percentage = item.$2 / ranked.first.$2;\n\n                final location = [\n                  Text(\n                    item.$1.name,\n                    style: TextStyle(\n                      fontSize: fontSize,\n                      fontWeight: index == 0\n                          ? FontWeight.bold\n                          : index < 3\n                          ? FontWeight.w500\n                          : null,\n                    ),\n                  ),\n                  const SizedBox(width: 8),\n                  Text(\n                    '${item.$1.county}${item.$1.town}',\n                    style: TextStyle(\n                      fontSize: fontSize / 1.25,\n                      color: foregroundColor.withValues(alpha: 0.8),\n                    ),\n                  ),\n                ];\n\n                final content = [\n                  Expanded(\n                    child: index < 3\n                        ? Column(\n                            crossAxisAlignment: CrossAxisAlignment.stretch,\n                            children: location,\n                          )\n                        : Row(children: location),\n                  ),\n                  Text(\n                    '${item.$2} mm',\n                    style: TextStyle(\n                      fontSize: fontSize,\n                      fontWeight: index == 0\n                          ? FontWeight.bold\n                          : index < 3\n                          ? FontWeight.w500\n                          : null,\n                    ),\n                  ),\n                ];\n\n                return Container(\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 4,\n                  ),\n                  child: Row(\n                    children: [\n                      SizedBox(width: 48, child: Center(child: leading)),\n                      const SizedBox(width: 8),\n                      Expanded(\n                        child: Container(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 16,\n                            vertical: 8,\n                          ),\n                          decoration: BoxDecoration(\n                            borderRadius: BorderRadius.circular(8),\n                            color: backgroundColor,\n                            gradient: LinearGradient(\n                              colors: [\n                                backgroundColor,\n                                backgroundColor,\n                                backgroundColor.withValues(alpha: 0.4),\n                                backgroundColor.withValues(alpha: 0.4),\n                              ],\n                              stops: [0, percentage, percentage, 1],\n                            ),\n                          ),\n                          child: Row(children: content),\n                        ),\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/ranking/tabs/temperature.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nenum MergeType { none, county, town }\n\nclass RankingTemperatureTab extends StatefulWidget {\n  const RankingTemperatureTab({super.key});\n\n  @override\n  State<RankingTemperatureTab> createState() => _RankingTemperatureTabState();\n}\n\nclass _RankingTemperatureTabState extends State<RankingTemperatureTab> {\n  MergeType merge = MergeType.none;\n  bool reversed = false;\n  String time = '';\n  List<WeatherStation> data = [];\n  List<WeatherStation> ranked = [];\n\n  Future<void> refresh() async {\n    final weatherList = await ExpTech().getWeatherList();\n    final latestWeatherData = await ExpTech().getWeather(weatherList.last);\n\n    if (!mounted) return;\n\n    data = latestWeatherData.where((station) => station.data.air.temperature != -99).toList();\n    time = DateFormat(\n      'yyyy/MM/dd HH:mm:ss',\n    ).format(parseDateTime(weatherList.last));\n    rank();\n  }\n\n  void rank() {\n    final temp = (merge != MergeType.none)\n        ? groupBy(\n            data,\n            (e) => merge == MergeType.town ? (e.station.county, e.station.town) : e.station.county,\n          ).values.map(\n            (v) => v.reduce(\n              (acc, e) =>\n                  (reversed\n                      ? e.data.air.temperature < acc.data.air.temperature\n                      : e.data.air.temperature > acc.data.air.temperature)\n                  ? e\n                  : acc,\n            ),\n          )\n        : data;\n\n    final sorted = temp\n        .sorted(\n          (a, b) => (b.data.air.temperature - a.data.air.temperature).sign.toInt(),\n        )\n        .toList();\n    setState(() {\n      ranked = reversed ? sorted.reversed.toList() : sorted;\n    });\n  }\n\n  void setMerge(MergeType state) {\n    if (state == merge) {\n      merge = MergeType.none;\n    } else {\n      merge = state;\n    }\n    rank();\n  }\n\n  void setReversed(bool state) {\n    reversed = state;\n    rank();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: refresh,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          SizedBox(\n            height: kToolbarHeight,\n            child: SingleChildScrollView(\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              scrollDirection: Axis.horizontal,\n              child: Wrap(\n                spacing: 8,\n                runAlignment: WrapAlignment.center,\n                crossAxisAlignment: WrapCrossAlignment.center,\n                children: [\n                  const Padding(\n                    padding: EdgeInsets.symmetric(horizontal: 4),\n                    child: Text('依'),\n                  ),\n                  ChoiceChip(\n                    label: const Text('最高'),\n                    selected: !reversed,\n                    onSelected: (value) => setReversed(false),\n                  ),\n                  ChoiceChip(\n                    label: const Text('最低'),\n                    selected: reversed,\n                    onSelected: (value) => setReversed(true),\n                  ),\n                  const SizedBox(\n                    height: kToolbarHeight - 16,\n                    child: VerticalDivider(),\n                  ),\n                  const Padding(\n                    padding: EdgeInsets.symmetric(horizontal: 4),\n                    child: Text('合併至'),\n                  ),\n                  ChoiceChip(\n                    label: const Text('鄉鎮'),\n                    selected: merge == MergeType.town,\n                    onSelected: (value) => setMerge(MergeType.town),\n                  ),\n                  ChoiceChip(\n                    label: const Text('縣市'),\n                    selected: merge == MergeType.county,\n                    onSelected: (value) => setMerge(MergeType.county),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n            child: Text(\n              '資料時間：$time\\n共 ${ranked.length} 觀測點',\n              style: TextStyle(color: context.colors.onSurfaceVariant),\n            ),\n          ),\n          Expanded(\n            child: ListView.builder(\n              padding: const EdgeInsets.symmetric(vertical: 4),\n              itemCount: ranked.isEmpty ? 1 : ranked.length,\n              itemBuilder: (context, index) {\n                if (ranked.isEmpty) {\n                  return const Padding(\n                    padding: EdgeInsets.only(top: 16),\n                    child: Center(child: CircularProgressIndicator()),\n                  );\n                }\n\n                final item = ranked[index];\n                final rank = index + 1;\n\n                final backgroundColor = index == 0\n                    ? context.theme.extendedColors.amberContainer\n                    : index == 1\n                    ? context.theme.extendedColors.greyContainer\n                    : index == 2\n                    ? context.theme.extendedColors.brownContainer\n                    : index < 10\n                    ? context.colors.surfaceContainerHigh\n                    : context.colors.surfaceContainer;\n\n                final foregroundColor = index == 0\n                    ? context.theme.extendedColors.onAmberContainer\n                    : index == 1\n                    ? context.colors.onSurface\n                    : index == 2\n                    ? context.theme.extendedColors.onBrownContainer\n                    : index < 10\n                    ? context.colors.onSurface\n                    : context.colors.onSurfaceVariant;\n\n                final iconColor = index == 0\n                    ? context.theme.extendedColors.amber\n                    : index == 1\n                    ? context.theme.extendedColors.grey\n                    : context.theme.extendedColors.brown;\n\n                final double fontSize = index == 0\n                    ? 20\n                    : index < 3\n                    ? 18\n                    : 16;\n\n                final double iconSize = index == 0\n                    ? 32\n                    : index == 1\n                    ? 28\n                    : 24;\n\n                final leading = index < 3\n                    ? Icon(\n                        index == 0 ? Symbols.trophy_rounded : Symbols.workspace_premium_rounded,\n                        color: iconColor,\n                        size: iconSize,\n                        fill: 1,\n                      )\n                    : Text(\n                        '$rank',\n                        style: TextStyle(\n                          color: foregroundColor,\n                          fontSize: fontSize,\n                        ),\n                      );\n\n                final percentage = reversed\n                    ? (ranked.first.data.air.temperature - item.data.air.temperature) /\n                          (ranked.first.data.air.temperature - ranked.last.data.air.temperature)\n                    : (item.data.air.temperature - ranked.last.data.air.temperature) /\n                          (ranked.first.data.air.temperature - ranked.last.data.air.temperature);\n\n                final location = merge != MergeType.none\n                    ? [\n                        Text(\n                          merge == MergeType.town\n                              ? '${item.station.county}${item.station.town}'\n                              : item.station.county,\n                          style: TextStyle(\n                            fontSize: fontSize,\n                            fontWeight: index == 0\n                                ? FontWeight.bold\n                                : index < 3\n                                ? FontWeight.w500\n                                : null,\n                          ),\n                        ),\n                      ]\n                    : [\n                        Text(\n                          item.station.name,\n                          style: TextStyle(\n                            fontSize: fontSize,\n                            fontWeight: index == 0\n                                ? FontWeight.bold\n                                : index < 3\n                                ? FontWeight.w500\n                                : null,\n                          ),\n                        ),\n                        const SizedBox(width: 8),\n                        Text(\n                          '${item.station.county}${item.station.town}',\n                          style: TextStyle(\n                            fontSize: fontSize / 1.25,\n                            color: foregroundColor.withValues(alpha: 0.8),\n                          ),\n                        ),\n                      ];\n\n                final content = [\n                  Expanded(\n                    child: index < 3\n                        ? Column(\n                            crossAxisAlignment: CrossAxisAlignment.stretch,\n                            children: location,\n                          )\n                        : Row(children: location),\n                  ),\n                  Text(\n                    '${item.data.air.temperature.toStringAsFixed(1)}℃',\n                    style: TextStyle(\n                      fontSize: fontSize,\n                      fontWeight: index == 0\n                          ? FontWeight.bold\n                          : index < 3\n                          ? FontWeight.w500\n                          : null,\n                    ),\n                  ),\n                ];\n\n                return Container(\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 4,\n                  ),\n                  child: Row(\n                    children: [\n                      SizedBox(width: 48, child: Center(child: leading)),\n                      const SizedBox(width: 8),\n                      Expanded(\n                        child: Container(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 16,\n                            vertical: 8,\n                          ),\n                          decoration: BoxDecoration(\n                            borderRadius: BorderRadius.circular(8),\n                            color: backgroundColor,\n                            gradient: LinearGradient(\n                              colors: [\n                                backgroundColor,\n                                backgroundColor,\n                                backgroundColor.withValues(alpha: 0.4),\n                                backgroundColor.withValues(alpha: 0.4),\n                              ],\n                              stops: [0, percentage, percentage, 1],\n                            ),\n                          ),\n                          child: Row(children: content),\n                        ),\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/ranking/tabs/wind.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nenum MergeType { none, county, town }\n\nclass RankingWindTab extends StatefulWidget {\n  const RankingWindTab({super.key});\n\n  @override\n  State<RankingWindTab> createState() => _RankingWindTabState();\n}\n\nclass _RankingWindTabState extends State<RankingWindTab> {\n  MergeType merge = MergeType.none;\n  bool reversed = false;\n  String time = '';\n  List<WeatherStation> data = [];\n  List<WeatherStation> ranked = [];\n\n  Future<void> refresh() async {\n    final weatherList = await ExpTech().getWeatherList();\n    final latestWeatherData = await ExpTech().getWeather(weatherList.last);\n\n    if (!mounted) return;\n\n    data = latestWeatherData.where((station) => station.data.wind.speed != -99).toList();\n    time = DateFormat(\n      'yyyy/MM/dd HH:mm:ss',\n    ).format(parseDateTime(weatherList.last));\n    rank();\n  }\n\n  void rank() {\n    final temp = (merge != MergeType.none)\n        ? groupBy(\n            data,\n            (e) => merge == MergeType.town ? (e.station.county, e.station.town) : e.station.county,\n          ).values.map(\n            (v) => v.reduce(\n              (acc, e) =>\n                  (reversed\n                      ? e.data.wind.speed < acc.data.wind.speed\n                      : e.data.wind.speed > acc.data.wind.speed)\n                  ? e\n                  : acc,\n            ),\n          )\n        : data;\n\n    final sorted = temp\n        .where((e) => e.data.wind.speed > 0)\n        .sorted((a, b) => (b.data.wind.speed - a.data.wind.speed).sign.toInt())\n        .toList();\n\n    setState(() {\n      ranked = reversed ? sorted.reversed.toList() : sorted;\n    });\n  }\n\n  void setMerge(MergeType state) {\n    if (state == merge) {\n      merge = MergeType.none;\n    } else {\n      merge = state;\n    }\n    rank();\n  }\n\n  void setReversed(bool state) {\n    reversed = state;\n    rank();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    refresh();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: refresh,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          SizedBox(\n            height: kToolbarHeight,\n            child: SingleChildScrollView(\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              scrollDirection: Axis.horizontal,\n              child: Wrap(\n                spacing: 8,\n                runAlignment: WrapAlignment.center,\n                crossAxisAlignment: WrapCrossAlignment.center,\n                children: [\n                  const Padding(\n                    padding: EdgeInsets.symmetric(horizontal: 4),\n                    child: Text('依'),\n                  ),\n                  ChoiceChip(\n                    label: const Text('降冪'),\n                    selected: !reversed,\n                    onSelected: (value) => setReversed(false),\n                  ),\n                  ChoiceChip(\n                    label: const Text('升冪'),\n                    selected: reversed,\n                    onSelected: (value) => setReversed(true),\n                  ),\n                  const SizedBox(\n                    height: kToolbarHeight - 16,\n                    child: VerticalDivider(),\n                  ),\n                  const Padding(\n                    padding: EdgeInsets.symmetric(horizontal: 4),\n                    child: Text('合併至'),\n                  ),\n                  ChoiceChip(\n                    label: const Text('鄉鎮'),\n                    selected: merge == MergeType.town,\n                    onSelected: (value) => setMerge(MergeType.town),\n                  ),\n                  ChoiceChip(\n                    label: const Text('縣市'),\n                    selected: merge == MergeType.county,\n                    onSelected: (value) => setMerge(MergeType.county),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n            child: Text(\n              '資料時間：$time\\n共 ${ranked.length} 觀測點',\n              style: TextStyle(color: context.colors.onSurfaceVariant),\n            ),\n          ),\n          Expanded(\n            child: ListView.builder(\n              padding: const EdgeInsets.symmetric(vertical: 4),\n              itemCount: ranked.isEmpty ? 1 : ranked.length,\n              itemBuilder: (context, index) {\n                if (ranked.isEmpty) {\n                  return const Padding(\n                    padding: EdgeInsets.only(top: 16),\n                    child: Center(child: CircularProgressIndicator()),\n                  );\n                }\n\n                final item = ranked[index];\n                final rank = index + 1;\n\n                final backgroundColor = index == 0\n                    ? context.theme.extendedColors.amberContainer\n                    : index == 1\n                    ? context.theme.extendedColors.greyContainer\n                    : index == 2\n                    ? context.theme.extendedColors.brownContainer\n                    : index < 10\n                    ? context.colors.surfaceContainerHigh\n                    : context.colors.surfaceContainer;\n\n                final foregroundColor = index == 0\n                    ? context.theme.extendedColors.onAmberContainer\n                    : index == 1\n                    ? context.colors.onSurface\n                    : index == 2\n                    ? context.theme.extendedColors.onBrownContainer\n                    : index < 10\n                    ? context.colors.onSurface\n                    : context.colors.onSurfaceVariant;\n\n                final iconColor = index == 0\n                    ? context.theme.extendedColors.amber\n                    : index == 1\n                    ? context.theme.extendedColors.grey\n                    : context.theme.extendedColors.brown;\n\n                final double fontSize = index == 0\n                    ? 20\n                    : index < 3\n                    ? 18\n                    : 16;\n\n                final double iconSize = index == 0\n                    ? 32\n                    : index == 1\n                    ? 28\n                    : 24;\n\n                final leading = index < 3\n                    ? Icon(\n                        index == 0 ? Symbols.trophy_rounded : Symbols.workspace_premium_rounded,\n                        color: iconColor,\n                        size: iconSize,\n                        fill: 1,\n                      )\n                    : Text(\n                        '$rank',\n                        style: TextStyle(\n                          color: foregroundColor,\n                          fontSize: fontSize,\n                        ),\n                      );\n\n                final minWind = reversed\n                    ? ranked.first.data.wind.speed\n                    : ranked.last.data.wind.speed;\n                final maxWind = reversed\n                    ? ranked.last.data.wind.speed\n                    : ranked.first.data.wind.speed;\n                final percentage = (item.data.wind.speed - minWind) / (maxWind - minWind);\n\n                final location = merge != MergeType.none\n                    ? [\n                        Text(\n                          merge == MergeType.town\n                              ? '${item.station.county}${item.station.town}'\n                              : item.station.county,\n                          style: TextStyle(\n                            fontSize: fontSize,\n                            fontWeight: index == 0\n                                ? FontWeight.bold\n                                : index < 3\n                                ? FontWeight.w500\n                                : null,\n                          ),\n                        ),\n                      ]\n                    : [\n                        Text(\n                          item.station.name,\n                          style: TextStyle(\n                            fontSize: fontSize,\n                            fontWeight: index == 0\n                                ? FontWeight.bold\n                                : index < 3\n                                ? FontWeight.w500\n                                : null,\n                          ),\n                        ),\n                        const SizedBox(width: 8),\n                        Text(\n                          '${item.station.county}${item.station.town}',\n                          style: TextStyle(\n                            fontSize: fontSize / 1.25,\n                            color: foregroundColor.withValues(alpha: 0.8),\n                          ),\n                        ),\n                      ];\n\n                final content = [\n                  Expanded(\n                    child: index < 3\n                        ? Column(\n                            crossAxisAlignment: CrossAxisAlignment.stretch,\n                            children: location,\n                          )\n                        : Row(children: location),\n                  ),\n                  Text(\n                    '${item.data.wind.speed.toStringAsFixed(1)} m/s',\n                    style: TextStyle(\n                      fontSize: fontSize,\n                      fontWeight: index == 0\n                          ? FontWeight.bold\n                          : index < 3\n                          ? FontWeight.w500\n                          : null,\n                    ),\n                  ),\n                ];\n\n                return Container(\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 4,\n                  ),\n                  child: Row(\n                    children: [\n                      SizedBox(width: 48, child: Center(child: leading)),\n                      const SizedBox(width: 8),\n                      Expanded(\n                        child: Container(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 16,\n                            vertical: 8,\n                          ),\n                          decoration: BoxDecoration(\n                            borderRadius: BorderRadius.circular(8),\n                            color: backgroundColor,\n                            gradient: LinearGradient(\n                              colors: [\n                                backgroundColor,\n                                backgroundColor,\n                                backgroundColor.withValues(alpha: 0.4),\n                                backgroundColor.withValues(alpha: 0.4),\n                              ],\n                              stops: [0, percentage, percentage, 1],\n                            ),\n                          ),\n                          child: Row(children: content),\n                        ),\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_old/page/more/report_list/report_list.dart",
    "content": "import 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/report/partial_earthquake_report.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/report/list_item.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass ReportListPage extends StatefulWidget {\n  const ReportListPage({super.key});\n\n  @override\n  State<ReportListPage> createState() => _ReportListPageState();\n}\n\nclass _ReportListPageState extends State<ReportListPage> {\n  final scroll = GlobalKey<NestedScrollViewState>();\n  List<PartialEarthquakeReport> reportList = [];\n  DateTime? lastFetchTime;\n  bool isLoading = false;\n  bool isLoadingEnd = false;\n  int _currentPage = 1;\n  int _loadedPage = 0;\n\n  RangeValues _intensityRange = const RangeValues(0, 8);\n  RangeValues _magnitudeRange = const RangeValues(0, 10);\n  RangeValues _depthRange = const RangeValues(0, 700);\n\n  List<String> get _intensityLevels => List.generate(9, (i) {\n    final count = i + 1;\n    const map = {5: '5弱', 6: '5強', 7: '6弱', 8: '6強', 9: '7級'};\n    return map[count] ?? '$count級';\n  });\n\n  Future<void> refreshReportList() async {\n    if (lastFetchTime != null && DateTime.now().difference(lastFetchTime!).inMinutes < 1) {\n      return;\n    }\n    if (isLoadingEnd) return;\n\n    setState(() {\n      isLoading = true;\n      _currentPage = 1;\n      _loadedPage = 0;\n      reportList.clear();\n    });\n\n    await _fetchNextPage();\n  }\n\n  void _loadMore() {\n    if (isLoading) return;\n    if (isLoadingEnd) return;\n    if (scroll.currentState == null) return;\n\n    if (scroll.currentState!.innerController.position.pixels ==\n        scroll.currentState!.innerController.position.maxScrollExtent) {\n      setState(() {\n        _currentPage++;\n        _fetchNextPage();\n      });\n    }\n  }\n\n  Future<void> _fetchNextPage() async {\n    if (isLoading) return;\n    if (_currentPage <= _loadedPage) {\n      setState(() {\n        _currentPage = _loadedPage;\n      });\n      return;\n    }\n\n    setState(() => isLoading = true);\n\n    final newList = await ExpTech().getReportList(\n      limit: 500,\n      page: _currentPage,\n      minIntensity: (_intensityRange.start + 1).round(),\n      maxIntensity: (_intensityRange.end + 1).round(),\n      minMagnitude: _magnitudeRange.start.round(),\n      maxMagnitude: _magnitudeRange.end.round(),\n      minDepth: _depthRange.start.round(),\n      maxDepth: _depthRange.end.round(),\n    );\n    addToList(newList);\n\n    setState(() {\n      _loadedPage = _currentPage;\n      isLoading = false;\n    });\n  }\n\n  void addToList(List<PartialEarthquakeReport> list) {\n    final oldIdList = reportList.map((v) => v.id);\n\n    list.removeWhere((r) => oldIdList.contains(r.id));\n\n    list = reportList + list;\n\n    list.sort((a, b) => b.time.difference(a.time).inMilliseconds);\n\n    setState(() {\n      reportList = list;\n      lastFetchTime = DateTime.now();\n    });\n  }\n\n  String _getFilterSummary() {\n    final List<String> summaries = [];\n\n    if (_intensityRange.start > 0 || _intensityRange.end < 8) {\n      summaries.add(\n        '最大震度: ${_intensityLevels[_intensityRange.start.round()]}-${_intensityLevels[_intensityRange.end.round()]}',\n      );\n    }\n    if (_magnitudeRange.start > 0 || (_magnitudeRange.end > 0 && _magnitudeRange.end < 10)) {\n      summaries.add(\n        '規模: ${_magnitudeRange.start.round()}-${_magnitudeRange.end.round()}',\n      );\n    }\n    if (_depthRange.start > 0 || (_depthRange.end > 0 && _depthRange.end < 700)) {\n      summaries.add(\n        '深度: ${_depthRange.start.round()}-${_depthRange.end.round()}km',\n      );\n    }\n\n    return summaries.isEmpty ? '全部' : summaries.join(', ');\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      scroll.currentState?.innerController.addListener(_loadMore);\n    });\n    _fetchNextPage();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return NestedScrollView(\n      headerSliverBuilder: (context, innerBoxIsScrolled) {\n        return [\n          SliverAppBar(\n            pinned: true,\n            floating: true,\n            title: const Text('地震報告'),\n            bottom: PreferredSize(\n              preferredSize: const Size.fromHeight(kToolbarHeight),\n              child: Container(\n                width: double.maxFinite,\n                padding: const EdgeInsets.symmetric(\n                  horizontal: 12,\n                  vertical: 4,\n                ),\n                child: Wrap(\n                  children: [\n                    FilterChip(\n                      label: Text(\n                        _getFilterSummary() == '全部' ? '篩選器' : _getFilterSummary(),\n                      ),\n                      selected: _getFilterSummary() != '全部',\n                      onSelected: (_) => _showFilterDialog(),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            shape: Border(\n              bottom: BorderSide(color: context.colors.outlineVariant),\n            ),\n          ),\n        ];\n      },\n      body: Column(\n        children: [\n          Visibility(\n            visible: isLoading,\n            child: LinearProgressIndicator(\n              borderRadius: BorderRadius.circular(4),\n              backgroundColor: context.colors.secondaryContainer,\n            ),\n          ),\n          Expanded(\n            child: RefreshIndicator(\n              onRefresh: refreshReportList,\n              child: Builder(\n                builder: (context) {\n                  return ListView.builder(\n                    padding: EdgeInsets.only(bottom: context.padding.bottom),\n                    itemCount: reportList.length + 1,\n                    itemBuilder: (context, index) {\n                      if (index == reportList.length) {\n                        if (!isLoading) {\n                          isLoadingEnd = true;\n                        } else if (isLoading) {\n                          isLoadingEnd = false;\n                        }\n                        return const Padding(\n                          padding: EdgeInsets.all(8.0),\n                          child: Center(child: Text('到底了')),\n                        );\n                      }\n\n                      var showDate = false;\n                      final current = reportList[index];\n\n                      if (index != 0) {\n                        final prev = reportList[index - 1];\n                        if (current.time.day != prev.time.day) {\n                          showDate = true;\n                        }\n                      } else {\n                        showDate = true;\n                      }\n\n                      return ReportListItem(\n                        report: current,\n                        showDate: showDate,\n                        first: index == 0,\n                        refreshReportList: refreshReportList,\n                      );\n                    },\n                  );\n                },\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _showFilterDialog() {\n    showDialog(\n      context: context,\n      builder: (context) {\n        return StatefulBuilder(\n          builder: (context, setState) {\n            return AlertDialog(\n              title: const Text('篩選器'),\n              content: SingleChildScrollView(\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    const Text('最大震度'),\n                    RangeSlider(\n                      values: _intensityRange,\n                      max: 8,\n                      divisions: 8,\n                      labels: RangeLabels(\n                        _intensityLevels[_intensityRange.start.round()],\n                        _intensityLevels[_intensityRange.end.round()],\n                      ),\n                      onChanged: (RangeValues values) {\n                        setState(() {\n                          _intensityRange = RangeValues(\n                            values.start.roundToDouble(),\n                            values.end.roundToDouble(),\n                          );\n                        });\n                      },\n                    ),\n                    const Text('規模'),\n                    RangeSlider(\n                      values: _magnitudeRange,\n                      max: 10,\n                      divisions: 10,\n                      labels: RangeLabels(\n                        _magnitudeRange.start.round().toString(),\n                        _magnitudeRange.end.round().toString(),\n                      ),\n                      onChanged: (RangeValues values) {\n                        setState(() {\n                          _magnitudeRange = RangeValues(\n                            values.start.roundToDouble(),\n                            values.end.roundToDouble(),\n                          );\n                        });\n                      },\n                    ),\n                    const Text('深度'),\n                    RangeSlider(\n                      values: _depthRange,\n                      max: 700,\n                      divisions: 70,\n                      labels: RangeLabels(\n                        '${_depthRange.start.round()}km',\n                        '${_depthRange.end.round()}km',\n                      ),\n                      onChanged: (RangeValues values) {\n                        setState(() {\n                          _depthRange = RangeValues(\n                            values.start.roundToDouble(),\n                            values.end.roundToDouble(),\n                          );\n                        });\n                      },\n                    ),\n                  ],\n                ),\n              ),\n              actions: [\n                TextButton(\n                  child: const Text('重置'),\n                  onPressed: () {\n                    setState(() {\n                      _intensityRange = const RangeValues(0, 8);\n                      _magnitudeRange = const RangeValues(0, 10);\n                      _depthRange = const RangeValues(0, 700);\n                    });\n                  },\n                ),\n                TextButton(\n                  child: const Text('套用'),\n                  onPressed: () {\n                    context.pop();\n                    this.setState(() {\n                      _currentPage = 1;\n                      _loadedPage = 0;\n                      reportList.clear();\n                      _fetchNextPage();\n                    });\n                  },\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/core/compass.dart",
    "content": "import 'dart:async';\n\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter_compass/flutter_compass.dart';\n\nclass CompassService {\n  CompassService._();\n\n  static final CompassService _instance = CompassService._();\n  static CompassService get instance => _instance;\n\n  StreamController<CompassEvent>? _controller;\n  StreamSubscription<CompassEvent>? _sourceSubscription;\n  double _lastHeading = 0.0;\n  bool _isInitialized = false;\n\n  Stream<CompassEvent>? get events => _controller?.stream;\n\n  double get lastHeading => _lastHeading;\n\n  bool get isInitialized => _isInitialized;\n\n  bool get hasCompass => _isInitialized && _controller != null;\n\n  Future<void> initialize() async {\n    if (_isInitialized) {\n      TalkerManager.instance.debug('CompassService: already initialized');\n      return;\n    }\n\n    TalkerManager.instance.debug('CompassService: initializing...');\n\n    try {\n      final sourceStream = FlutterCompass.events;\n      if (sourceStream == null) {\n        TalkerManager.instance.debug('CompassService: compass not available');\n        _isInitialized = true;\n        return;\n      }\n\n      _controller = StreamController<CompassEvent>.broadcast(\n        onListen: () {\n          TalkerManager.instance.debug('CompassService: first listener added');\n        },\n        onCancel: () {\n          TalkerManager.instance.debug('CompassService: last listener removed');\n        },\n      );\n\n      _sourceSubscription = sourceStream.listen(\n        (event) {\n          if (event.heading != null) {\n            _lastHeading = event.heading!;\n          }\n          _controller?.add(event);\n        },\n        onError: (Object error) {\n          TalkerManager.instance.error('CompassService: stream error', error);\n          _controller?.addError(error);\n        },\n        onDone: () {\n          TalkerManager.instance.debug('CompassService: source stream done');\n          _controller?.close();\n        },\n      );\n\n      _isInitialized = true;\n      TalkerManager.instance.debug('CompassService: initialized successfully');\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'CompassService: initialization failed',\n        e,\n        s,\n      );\n      _isInitialized = true;\n    }\n  }\n\n  Future<void> dispose() async {\n    TalkerManager.instance.debug('CompassService: disposing...');\n    await _sourceSubscription?.cancel();\n    _sourceSubscription = null;\n    await _controller?.close();\n    _controller = null;\n    _isInitialized = false;\n  }\n}\n"
  },
  {
    "path": "lib/core/device_info.dart",
    "content": "import 'dart:io';\n\nimport 'package:device_info_plus/device_info_plus.dart';\n\nmixin DeviceInfo {\n  static late String model;\n  static late String version;\n  static String? serial;\n\n  static Future init() async {\n    final deviceInfo = DeviceInfoPlugin();\n    if (Platform.isAndroid) {\n      final info = await deviceInfo.androidInfo;\n      model = info.model;\n      serial = info.id;\n      version = info.version.sdkInt.toString();\n    }\n    if (Platform.isIOS) {\n      final info = await deviceInfo.iosInfo;\n      final String machineCode = info.utsname.machine;\n      model = _iphoneModelMap(machineCode);\n      serial = info.identifierForVendor;\n      version = info.systemVersion;\n    }\n  }\n\n  static String _iphoneModelMap(String code) {\n    const groupedModels = <List<String>, String>{\n      ['iPhone8,1']: 'iPhone 6s',\n      ['iPhone8,2']: 'iPhone 6s Plus',\n      ['iPhone8,4']: 'iPhone SE\\n(1st Gen)',\n      ['iPhone9,1', 'iPhone9,3']: 'iPhone 7',\n      ['iPhone9,2', 'iPhone9,4']: 'iPhone 7 Plus',\n      ['iPhone10,1', 'iPhone10,4']: 'iPhone 8',\n      ['iPhone10,2', 'iPhone10,5']: 'iPhone 8 Plus',\n      ['iPhone10,3', 'iPhone10,6']: 'iPhone X',\n      ['iPhone11,2']: 'iPhone XS',\n      ['iPhone11,4', 'iPhone11,6']: 'iPhone XS Max',\n      ['iPhone11,8']: 'iPhone XR',\n      ['iPhone12,1']: 'iPhone 11',\n      ['iPhone12,3']: 'iPhone 11 Pro',\n      ['iPhone12,5']: 'iPhone 11 Pro Max',\n      ['iPhone12,8']: 'iPhone SE\\n(2nd Gen)',\n      ['iPhone13,2']: 'iPhone 12',\n      ['iPhone13,1']: 'iPhone 12 mini',\n      ['iPhone13,3']: 'iPhone 12 Pro',\n      ['iPhone13,4']: 'iPhone 12 Pro Max',\n      ['iPhone14,5']: 'iPhone 13',\n      ['iPhone14,4']: 'iPhone 13 mini',\n      ['iPhone14,2']: 'iPhone 13 Pro',\n      ['iPhone14,3']: 'iPhone 13 Pro Max',\n      ['iPhone14,6']: 'iPhone SE\\n(3rd Gen)',\n      ['iPhone14,7']: 'iPhone 14',\n      ['iPhone14,8']: 'iPhone 14 Plus',\n      ['iPhone15,2']: 'iPhone 14 Pro',\n      ['iPhone15,3']: 'iPhone 14 Pro Max',\n      ['iPhone15,4']: 'iPhone 15',\n      ['iPhone15,5']: 'iPhone 15 Plus',\n      ['iPhone16,1']: 'iPhone 15 Pro',\n      ['iPhone16,2']: 'iPhone 15 Pro Max',\n      ['iPhone17,3']: 'iPhone 16',\n      ['iPhone17,4']: 'iPhone 16 Plus',\n      ['iPhone17,1']: 'iPhone 16 Pro',\n      ['iPhone17,2']: 'iPhone 16 Pro Max',\n      ['iPhone17,5']: 'iPhone 16e',\n\n      // iPad\n      ['iPad6,11', 'iPad6,12']: 'iPad 5',\n      ['iPad7,5', 'iPad7,6']: 'iPad 6',\n      ['iPad7,11', 'iPad7,12']: 'iPad 7',\n      ['iPad11,6', 'iPad11,7']: 'iPad 8',\n      ['iPad12,1', 'iPad12,2']: 'iPad 9',\n      ['iPad13,18', 'iPad13,19']: 'iPad 10',\n      ['iPad15,7', 'iPad15,8']: 'iPad 11',\n      // iPad Air\n      ['iPad5,3', 'iPad5,4']: 'iPad Air 2',\n      ['iPad11,3', 'iPad11,4']: 'iPad Air 3',\n      ['iPad13,1', 'iPad13,2']: 'iPad Air 4',\n      ['iPad13,16', 'iPad13,17']: 'iPad Air 5',\n      ['iPad14,8', 'iPad14,9']: 'iPad Air 11-Inch M2',\n      ['iPad14,10', 'iPad14,11']: 'iPad Air 13-Inch M2',\n      ['iPad15,3', 'iPad15,4']: 'iPad Air 11-Inch M3',\n      ['iPad15,5', 'iPad15,6']: 'iPad Air 13-Inch M3',\n      // iPad Mini\n      ['iPad5,1', 'iPad5,2']: 'iPad Mini 4',\n      ['iPad11,1', 'iPad11,2']: 'iPad Mini 5',\n      ['iPad14,1', 'iPad14,2']: 'iPad Mini 6',\n      ['iPad16,1', 'iPad16,2']: 'iPad Mini 7',\n      // iPad Pro\n      ['iPad6,3', 'iPad6,4']: 'iPad Pro 9-Inch',\n      ['iPad7,3', 'iPad7,4']: 'iPad Pro 10-Inch',\n      ['iPad8,1', 'iPad8,2', 'iPad8,3', 'iPad8,4']: 'iPad Pro 11-Inch',\n      ['iPad8,9', 'iPad8,10']: 'iPad Pro 11-Inch 2',\n      ['iPad13,4', 'iPad13,5', 'iPad13,6', 'iPad13,7']: 'iPad Pro 11-Inch 3',\n      ['iPad14,3', 'iPad14,4']: 'iPad Pro 11-Inch 4',\n      ['iPad16,3', 'iPad16,4']: 'iPad Pro 11-Inch (M4)',\n      ['iPad6,7', 'iPad6,8']: 'iPad Pro 12-Inch',\n      ['iPad7,1', 'iPad7,2']: 'iPad Pro 12-Inch 2',\n      ['iPad8,5', 'iPad8,6', 'iPad8,7', 'iPad8,8']: 'iPad Pro 12-Inch 3',\n      ['iPad8,11', 'iPad8,12']: 'iPad Pro 12-Inch 4',\n      ['iPad13,8', 'iPad13,9', 'iPad13,10', 'iPad13,11']: 'iPad Pro 12-Inch 5',\n      ['iPad14,5', 'iPad14,6']: 'iPad Pro 12-Inch 6',\n      ['iPad16,5', 'iPad16,6']: 'iPad Pro 13-Inch (M4)',\n    };\n    for (final entry in groupedModels.entries) {\n      if (entry.key.contains(code)) {\n        return entry.value;\n      }\n    }\n    return code;\n  }\n}\n"
  },
  {
    "path": "lib/core/eew.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/api/model/wave_time.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/iterable.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nconst double ln10 = 2.302585092994046; // Math.log(10)\n\n({double p, double s, double sT}) calcWaveRadius(\n  double depth,\n  int time,\n  int now,\n) {\n  double pDist = 0;\n  double sDist = 0;\n  double sT = 0;\n\n  final double t = (now - time) / 1000.0;\n\n  final timeTable =\n      Global.timeTable[findClosest(\n        Global.timeTable.keys.map(int.parse).toList(),\n        depth,\n      ).toString()]!;\n  ({double P, double R, double S})? prevTable;\n\n  for (final table in timeTable) {\n    if (pDist == 0 && table.P > t) {\n      if (prevTable != null) {\n        final double tDiff = table.P - prevTable.P;\n        final double rDiff = table.R - prevTable.R;\n        final double tOffset = t - prevTable.P;\n        final double rOffset = (tOffset / tDiff) * rDiff;\n        pDist = prevTable.R + rOffset;\n      } else {\n        pDist = table.R;\n      }\n    }\n\n    if (sDist == 0 && table.S > t) {\n      if (prevTable != null) {\n        final double tDiff = table.S - prevTable.S;\n        final double rDiff = table.R - prevTable.R;\n        final double tOffset = t - prevTable.S;\n        final double rOffset = (tOffset / tDiff) * rDiff;\n        sDist = prevTable.R + rOffset;\n      } else {\n        sDist = table.R;\n        sT = table.S;\n      }\n    }\n\n    if (pDist != 0 && sDist != 0) break;\n    prevTable = table;\n  }\n\n  if (pDist < 0) {\n    pDist = 0;\n  }\n  if (sDist < 0) {\n    sDist = 0;\n  }\n\n  return (p: pDist, s: sDist, sT: sT);\n}\n\nint findClosest(List<int> arr, double target) {\n  return arr.reduce(\n    (prev, curr) => (curr - target).abs() < (prev - target).abs() ? curr : prev,\n  );\n}\n\nMap<String, dynamic> eewAreaPga(\n  double lat,\n  double lon,\n  double depth,\n  double mag,\n  Map<String, Location> region,\n) {\n  final Map<String, dynamic> json = {};\n  double eewMaxI = 0.0;\n\n  region.forEach((String key, Location info) {\n    final double distSurface = LatLng(lat, lon).to(LatLng(info.lat, info.lng)) / 1000;\n    final double dist = sqrt(pow(distSurface, 2) + pow(depth, 2));\n    final double pga = 1.657 * exp(1.533 * mag) * pow(dist, -1.607);\n    double i = pgaToFloat(pga);\n    if (i >= 4.5) {\n      i = eewAreaPgv([lat, lon], [info.lat, info.lng], depth, mag);\n    }\n    if (i > eewMaxI) {\n      eewMaxI = i;\n    }\n    json[key] = {'dist': dist, 'i': i};\n  });\n\n  json['max_i'] = eewMaxI;\n  return json;\n}\n\ndouble eewAreaPgv(\n  List<double> epicenterLocation,\n  List<double> pointLocation,\n  double depth,\n  double magW,\n) {\n  final double long = pow(10, 0.5 * magW - 1.85).toDouble() / 2;\n  final double epicenterDistance =\n      epicenterLocation.asLatLng.to(\n        pointLocation.asLatLng,\n      ) /\n      1000;\n  final double hypocenterDistance = sqrt(pow(depth, 2) + pow(epicenterDistance, 2)) - long;\n  final double x = max(hypocenterDistance, 3);\n  final double gpv600 = pow(\n    10,\n    0.58 * magW + 0.0038 * depth - 1.29 - log(x + 0.0028 * pow(10, 0.5 * magW)) / ln10 - 0.002 * x,\n  ).toDouble();\n  final double pgv400 = gpv600 * 1.31;\n  final double pgv = pgv400 * 1.0;\n  return 2.68 + 1.72 * log(pgv) / ln10;\n}\n\ndouble sWaveTimeByDistance(double depth, double sDist) {\n  double sTime = 0.0;\n\n  final timeTable =\n      Global.timeTable[findClosest(\n        Global.timeTable.keys.map(int.parse).toList(),\n        depth,\n      ).toString()]!;\n  ({double P, double R, double S})? prevTable;\n\n  for (final table in timeTable) {\n    if (sTime == 0 && table.R >= sDist) {\n      if (prevTable != null) {\n        final double rDiff = table.R - prevTable.R;\n        final double tDiff = table.S - prevTable.S;\n        final double rOffset = sDist - prevTable.R;\n        final double tOffset = (rOffset / rDiff) * tDiff;\n        sTime = prevTable.S + tOffset;\n      } else {\n        sTime = table.S;\n      }\n    }\n\n    if (sTime != 0) break;\n    prevTable = table;\n  }\n\n  return sTime * 1000;\n}\n\ndouble pWaveTimeByDistance(double depth, double pDist) {\n  double pTime = 0.0;\n\n  final timeTable =\n      Global.timeTable[findClosest(\n        Global.timeTable.keys.map(int.parse).toList(),\n        depth,\n      ).toString()]!;\n  ({double P, double R, double S})? prevTable;\n\n  for (final table in timeTable) {\n    if (pTime == 0 && table.R >= pDist) {\n      if (prevTable != null) {\n        final double rDiff = table.R - prevTable.R;\n        final double tDiff = table.P - prevTable.P;\n        final double rOffset = pDist - prevTable.R;\n        final double tOffset = (rOffset / rDiff) * tDiff;\n        pTime = prevTable.P + tOffset;\n      } else {\n        pTime = table.P;\n      }\n    }\n\n    if (pTime != 0) break;\n    prevTable = table;\n  }\n\n  return pTime * 1000;\n}\n\ndouble pgaToFloat(double pga) {\n  return 2 * (log(pga) / log(10)) + 0.7;\n}\n\nint pgaToIntensity(double pga) {\n  return intensityFloatToInt(pgaToFloat(pga));\n}\n\nint intensityFloatToInt(double floatValue) {\n  if (floatValue < 0.5) {\n    return 0;\n  } else if (floatValue < 1.5) {\n    return 1;\n  } else if (floatValue < 2.5) {\n    return 2;\n  } else if (floatValue < 3.5) {\n    return 3;\n  } else if (floatValue < 4.5) {\n    return 4;\n  } else if (floatValue < 5.0) {\n    return 5; // 5弱\n  } else if (floatValue < 5.5) {\n    return 6; // 5強\n  } else if (floatValue < 6.0) {\n    return 7; // 6弱\n  } else if (floatValue < 6.5) {\n    return 8; // 6強\n  } else {\n    return 9; // 7\n  }\n}\n\nString intensityToNumberString(int level) {\n  return (level == 5)\n      ? '5⁻'\n      : (level == 6)\n      ? '5⁺'\n      : (level == 7)\n      ? '6⁻'\n      : (level == 8)\n      ? '6⁺'\n      : (level == 9)\n      ? '7'\n      : level.toString();\n}\n\nWaveTime calculateWaveTime(double depth, double distance) {\n  final double za = 1 * depth;\n  double g0;\n  double G;\n  final double xb = distance;\n  if (depth <= 40) {\n    g0 = 5.10298;\n    G = 0.06659;\n  } else {\n    g0 = 7.804799;\n    G = 0.004573;\n  }\n  final double zc = -1 * (g0 / G);\n  final double xc = (pow(xb, 2) - 2 * (g0 / G) * za - pow(za, 2)) / (2 * xb);\n  double thetaA = atan((za - zc) / xc);\n  if (thetaA < 0) {\n    thetaA = thetaA + pi;\n  }\n  thetaA = pi - thetaA;\n  final double thetaB = atan(-1 * zc / (xb - xc));\n  double ptime = (1 / G) * log(tan(thetaA / 2) / tan(thetaB / 2));\n  final double g0_ = g0 / sqrt(3);\n  final double g_ = G / sqrt(3);\n  final double zc_ = -1 * (g0_ / g_);\n  final double xc_ = (pow(xb, 2) - 2 * (g0_ / g_) * za - pow(za, 2)) / (2 * xb);\n  double thetaA_ = atan((za - zc_) / xc_);\n  if (thetaA_ < 0) {\n    thetaA_ = thetaA_ + pi;\n  }\n  thetaA_ = pi - thetaA_;\n  final double thetaB_ = atan(-1 * zc_ / (xb - xc_));\n  double stime = (1 / g_) * log(tan(thetaA_ / 2) / tan(thetaB_ / 2));\n  if (distance / ptime > 7) {\n    ptime = distance / 7;\n  }\n  if (distance / stime > 4) {\n    stime = distance / 4;\n  }\n  return WaveTime(p: ptime, s: stime);\n}\n\n({double dist, double i}) eewLocationInfo(\n  double mag,\n  double depth,\n  double eqLat,\n  double eqLng,\n  double userLat,\n  double userLon,\n) {\n  final distSurface = LatLng(eqLat, eqLng).to(LatLng(userLat, userLon)) / 1000;\n  final dist = sqrt(pow(distSurface, 2) + pow(depth, 2));\n  final pga = 1.657 * exp(1.533 * mag) * pow(dist, -1.607);\n  var intensity = pgaToFloat(pga);\n  if (intensity >= 4.5) {\n    intensity = eewAreaPgv([eqLat, eqLng], [userLat, userLon], depth, mag);\n  }\n  return (dist: dist, i: intensity);\n}\n"
  },
  {
    "path": "lib/core/fcm.dart",
    "content": "import 'dart:io';\n\nimport 'package:awesome_notifications/awesome_notifications.dart';\nimport 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/main.dart';\nimport 'package:firebase_core/firebase_core.dart';\nimport 'package:firebase_messaging/firebase_messaging.dart';\nimport 'package:flutter/foundation.dart';\n\nFuture<void> fcmInit() async {\n  await Firebase.initializeApp();\n  if (Platform.isAndroid) {\n    await AwesomeNotificationsFcm().initialize(\n      onFcmTokenHandle: onTokenHandle,\n      onNativeTokenHandle: onTokenHandle,\n      onFcmSilentDataHandle: onFcmSilentDataHandle,\n      licenseKeys: [\n        '2024-08-26==N0LtVWu49ox9yV8eaDrh8rGyji/iKzLaB6anluLFIPESM/rUtf0OTUDyExMB+hp8YqnfA9UMcwvT5i5lTcsB73WKbh2+cYbYwtSZoxjuSUUbNxzhnlH2uiD7CNYvtniORC69TStgEfXnYZ1dEfWe5p6Nwi4wDS7vTfyTOH2NqCW+5293ypcu6+7se2PxLGOF1s8YKbM3HU8nYk8juChbFNoxX/Y0pHOH+MvXi070o1+3SPL98BS9bPQQ0e9a9MgYpxRqthP/mT1Yx2AX4+d+Qb6NNiz8ub+rl1HhZc7vmy5bntJSwcculDhXG3YOP3uXeYYyc2L+NKqkHPYpflblOg==',\n      ],\n      debug: kDebugMode,\n    );\n    await AwesomeNotificationsFcm().requestFirebaseAppToken();\n  } else if (Platform.isIOS) {\n    Preference.notifyToken = await FirebaseMessaging.instance.getAPNSToken();\n    if (!fcmReadyCompleter.isCompleted) {\n      fcmReadyCompleter.complete();\n    }\n  }\n}\n\nFuture<void> onTokenHandle(String token) async {\n  Preference.notifyToken = token;\n}\n\nFuture<void> onFcmSilentDataHandle(FcmSilentData silentData) async {\n  final Map<String, dynamic> data = silentData.data!.cast<String, dynamic>();\n\n  if (silentData.createdLifeCycle == NotificationLifeCycle.Terminated) {\n    final channelKey = (data['channel'] as String?) ?? 'other';\n    data['content'] = {\n      'id': int.parse((data['id'] as String?) ?? '0'),\n      'channelKey': channelKey,\n      'title': data['title'] as String?,\n      'body': data['body'] as String?,\n      'wakeUpScreen': true,\n      'category': NotificationCategory.Alarm,\n    };\n    await AwesomeNotifications().createNotificationFromJsonData(data);\n  } else {\n    await showNotify(data);\n  }\n}\n\nFuture<void> showNotify(Map<String, dynamic> data) async {\n  final channelKey = (data['channel'] as String?) ?? 'other';\n\n  await AwesomeNotifications().createNotification(\n    content: NotificationContent(\n      id: int.parse((data['id'] as String?) ?? '0'),\n      channelKey: channelKey,\n      title: data['title'] as String?,\n      body: data['body'] as String?,\n      wakeUpScreen: true,\n      category: NotificationCategory.Alarm,\n    ),\n  );\n}\n"
  },
  {
    "path": "lib/core/gps_location.dart",
    "content": "import 'dart:async';\n\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/map_utils.dart';\nimport 'package:geolocator/geolocator.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nCompleter<void>? _completer;\nDateTime? _lastUpdateTime;\n\nconst _kMinUpdateInterval = Duration(seconds: 30);\n\nFuture<void> updateLocationFromGPS() async {\n  if (_completer != null && !_completer!.isCompleted) {\n    return _completer!.future;\n  }\n\n  final now = DateTime.now();\n  if (_lastUpdateTime != null && now.difference(_lastUpdateTime!) < _kMinUpdateInterval) {\n    TalkerManager.instance.debug(\n      '📍 [GPS] Skipping update (throttle: ${now.difference(_lastUpdateTime!).inSeconds}s)',\n    );\n    return;\n  }\n\n  _completer = Completer();\n\n  try {\n    final serviceEnabled = await Geolocator.isLocationServiceEnabled();\n    if (!serviceEnabled) {\n      TalkerManager.instance.debug('📍 [GPS] Location services are disabled');\n      _clearLocation();\n      return;\n    }\n\n    final permission = await Geolocator.checkPermission();\n    if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {\n      TalkerManager.instance.debug('📍 [GPS] Location permission denied');\n      _clearLocation();\n      return;\n    }\n\n    Position? position = await Geolocator.getLastKnownPosition();\n    if (position == null) {\n      TalkerManager.instance.debug(\n        '📍 [GPS] No last known position, getting current position',\n      );\n      position = await Geolocator.getCurrentPosition(\n        locationSettings: const LocationSettings(\n          accuracy: LocationAccuracy.medium,\n          timeLimit: Duration(seconds: 10),\n        ),\n      );\n    }\n\n    final coordinates = LatLng(position.latitude, position.longitude);\n    final code = getTownCodeFromCoordinates(coordinates);\n\n    TalkerManager.instance.debug(\n      '📍 [GPS] Updated location: (${position.latitude}, ${position.longitude}) → code: $code',\n    );\n\n    GlobalProviders.location.setCoordinates(coordinates);\n    GlobalProviders.location.setCode(code);\n\n    _lastUpdateTime = now;\n  } catch (e, s) {\n    TalkerManager.instance.error('📍 [GPS] Error getting location', e, s);\n  } finally {\n    _completer?.complete();\n    _completer = null;\n  }\n}\n\n@Deprecated('Use updateLocationFromGPS() instead')\nFuture<void> updateSavedLocationIOS() => updateLocationFromGPS();\n\nvoid _clearLocation() {\n  GlobalProviders.location.setCoordinates(null);\n  GlobalProviders.location.setCode(null);\n}\n"
  },
  {
    "path": "lib/core/i18n.dart",
    "content": "import 'dart:convert';\nimport 'dart:ui';\n\nimport 'package:flutter/services.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\n\nclass I18nCsvLoader {\n  I18nCsvLoader._();\n\n  static Future<Map<String, Map<String, String>>> fromFile(String path) async {\n    final csvContent = await rootBundle.loadString(path);\n    return _parseCsvTranslations(csvContent);\n  }\n\n  static Map<String, Map<String, String>> _parseCsvTranslations(\n    String csvContent,\n  ) {\n    final lines = LineSplitter.split(csvContent);\n    if (lines.isEmpty) return {};\n\n    // Parse headers\n    final headers = _parseCsvLine(lines.first);\n    if (headers.isEmpty || headers[0] != 'key') {\n      throw const FormatException('CSV must start with \"key\" column');\n    }\n\n    // Extract language codes from headers (skip 'key' column)\n    final languageCodes = headers.skip(1).toList();\n\n    // Initialize result map with empty language maps\n    final Map<String, Map<String, String>> result = {};\n    for (final langCode in languageCodes) {\n      result[langCode] = <String, String>{};\n    }\n\n    // Process data rows\n    for (final line in lines.skip(1)) {\n      if (line.trim().isEmpty) continue;\n\n      final values = _parseCsvLine(line);\n      if (values.isEmpty || values[0].isEmpty) continue;\n\n      final key = values[0];\n      final translations = values.skip(1).toList();\n\n      result[key] = {\n        for (final langCode in languageCodes)\n          langCode: translations[languageCodes.indexOf(langCode)],\n      };\n    }\n\n    return result;\n  }\n\n  /// Parse a CSV line, handling quoted fields and commas\n  static List<String> _parseCsvLine(String line) {\n    final List<String> result = [];\n    final StringBuffer current = StringBuffer();\n    bool inQuotes = false;\n    bool lastWasQuote = false;\n\n    for (int i = 0; i < line.length; i++) {\n      final char = line[i];\n\n      if (char == '\"') {\n        if (inQuotes && lastWasQuote) {\n          // Double quote inside quoted field - add single quote\n          current.write('\"');\n          lastWasQuote = false;\n        } else if (inQuotes) {\n          // End of quoted field\n          lastWasQuote = true;\n        } else {\n          // Start of quoted field\n          inQuotes = true;\n          lastWasQuote = false;\n        }\n      } else if (char == ',' && (!inQuotes || lastWasQuote)) {\n        // Field separator\n        result.add(current.toString());\n        current.clear();\n        inQuotes = false;\n        lastWasQuote = false;\n      } else {\n        // Regular character\n        current.write(char);\n        lastWasQuote = false;\n      }\n    }\n\n    // Add final field\n    result.add(current.toString());\n\n    return result;\n  }\n}\n\nextension AppLocalizations on String {\n  /// The current locale used by the app.\n  static Locale _locale = 'zh-Hant'.asLocale;\n\n  /// The language tag of the current locale used by the app.\n  static String get languageTag => _locale.toLanguageTag();\n\n  /// Sets the current locale used by the app.\n  /// If the value is null, the current locale will not be changed.\n  static set locale(Locale? value) {\n    if (value == null) return;\n    _locale = value;\n  }\n\n  static final _t = Translations.byFile('zh-Hant', dir: 'assets/translations');\n  static Future<void> load() => _t.load();\n  String get i18n => localize(this, _t, languageTag: languageTag);\n}\n\nextension LocationNameLocalizations on String {\n  static late final Translations _locationNames;\n  static bool _isLoaded = false;\n\n  static Future<void> load() async {\n    if (_isLoaded) return;\n\n    final translations = await I18nCsvLoader.fromFile(\n      'assets/translations/location_names.csv',\n    );\n\n    _locationNames = Translations.byId('zh-Hant', translations);\n    _isLoaded = true;\n  }\n\n  String get locationName =>\n      localize(this, _locationNames, languageTag: AppLocalizations.languageTag);\n}\n\nextension WeatherStationLocalizations on String {\n  static late final Translations _weatherStations;\n  static bool _isLoaded = false;\n\n  static Future<void> load() async {\n    if (_isLoaded) return;\n\n    final translations = await I18nCsvLoader.fromFile(\n      'assets/translations/weather_station_names.csv',\n    );\n\n    _weatherStations = Translations.byId('zh-Hant', translations);\n    _isLoaded = true;\n  }\n\n  String get weatherStation => localize(\n    this,\n    _weatherStations,\n    languageTag: AppLocalizations.languageTag,\n  );\n}\n"
  },
  {
    "path": "lib/core/notify.dart",
    "content": "import 'dart:async';\n\nimport 'package:awesome_notifications/awesome_notifications.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/router.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/material.dart';\n\nconst int _notificationChannelVersion = 2;\n\nconst String _channelVersionKey = 'notification_channel_version';\n\nString? _pendingChannelKey;\n\n@pragma('vm:entry-point')\nFuture<void> onActionReceivedMethod(ReceivedAction receivedAction) async {\n  TalkerManager.instance.debug('Received action: $receivedAction');\n  final context = router.routerDelegate.navigatorKey.currentContext;\n  if (context == null) {\n    _pendingChannelKey = receivedAction.channelKey;\n    TalkerManager.instance.debug(\n      'Context not available, stored pending notification: channelKey=$_pendingChannelKey',\n    );\n    return;\n  }\n\n  final channelKey = receivedAction.channelKey;\n  TalkerManager.instance.debug('Notification clicked: channelKey=$channelKey');\n\n  _navigateBasedOnChannelKey(context, channelKey);\n}\n\nvoid handlePendingNotificationNavigation(BuildContext context) {\n  if (_pendingChannelKey == null) return;\n\n  TalkerManager.instance.debug(\n    'Handling pending notification: channelKey=$_pendingChannelKey',\n  );\n\n  _navigateBasedOnChannelKey(context, _pendingChannelKey);\n  _pendingChannelKey = null;\n}\n\nvoid _navigateBasedOnChannelKey(BuildContext context, String? channelKey) {\n  if (channelKey == null) return;\n\n  TalkerManager.instance.debug('Navigating based on channelKey: $channelKey');\n\n  if (channelKey.startsWith('eew') ||\n      channelKey.startsWith('int_report') ||\n      channelKey.startsWith('eq')) {\n    MapRoute(layers: 'monitor').push(context);\n    return;\n  }\n\n  if (channelKey.startsWith('report')) {\n    MapRoute(layers: 'report').push(context);\n    return;\n  }\n\n  // if (channelKey.startsWith('announcement')) {\n  //   AnnouncementRoute().push(context);\n  //   return;\n  // }\n\n  HomeRoute().go(context);\n}\n\nfinal List<NotificationChannel> _notificationChannels = [\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew_alert-important-v2',\n    channelName: '緊急地震速報(重大)',\n    channelDescription: '最大震度 5 弱以上以及所在地(鄉鎮)預估震度 4 以上',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/eew_alert',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew_alert-general-v2',\n    channelName: '緊急地震速報(一般)',\n    channelDescription: '最大震度 5 弱以上以及所在地(鄉鎮)預估震度 2 以上',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/eew',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew_alert-silent-v2',\n    channelName: '緊急地震速報(無聲通知)',\n    channelDescription: '最大震度 5 弱以上以及所在地(鄉鎮)預估震度 1 以上',\n    importance: NotificationImportance.Low,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: false,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew-important-v2',\n    channelName: '地震速報(重大)',\n    channelDescription: '所在地(鄉鎮)預估震度 4 以上',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/eew',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew-general-v2',\n    channelName: '地震速報(一般)',\n    channelDescription: '所在地(鄉鎮)預估震度 2 以上',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/eew',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eew',\n    channelKey: 'eew-silence-v2',\n    channelName: '地震速報 (無聲通知)',\n    channelDescription: '所在地(鄉鎮)預估震度 1 以上',\n    importance: NotificationImportance.Low,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: false,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eq',\n    channelKey: 'int_report-general-v2',\n    channelName: '震度速報(一般)',\n    channelDescription: '所在地(鄉鎮)實測震度 3 以上',\n    importance: NotificationImportance.High,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/report',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eq',\n    channelKey: 'int_report-silence-v2',\n    channelName: '震度速報 (無聲通知)',\n    channelDescription: '所在地(鄉鎮)實測震度 1 以上',\n    importance: NotificationImportance.Low,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: false,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eq',\n    channelKey: 'eq-v2',\n    channelName: '強震監視器(一般)',\n    channelDescription: '偵測到晃動',\n    importance: NotificationImportance.High,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/eq',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eq',\n    channelKey: 'report-general-v2',\n    channelName: '地震報告(一般)',\n    channelDescription: '地震報告所在地震度 3 以上',\n    importance: NotificationImportance.Default,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/report',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_eq',\n    channelKey: 'report-silence-v2',\n    channelName: '地震報告 (無聲通知)',\n    channelDescription: '地震報告所在地震度 1 以上',\n    groupAlertBehavior: GroupAlertBehavior.Children,\n    importance: NotificationImportance.Min,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: false,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: false,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'thunderstorm-important-v2',\n    channelName: '山區暴雨(重大)',\n    channelDescription: '所在地(鄉鎮)發布山區暴雨時',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/rain',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'thunderstorm-general-v2',\n    channelName: '雷雨即時訊息(一般)',\n    channelDescription: '所在地(鄉鎮)發布雷雨即時訊息時',\n    importance: NotificationImportance.High,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/rain',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'weather_major-important-v2',\n    channelName: '天氣警特報(重大)',\n    channelDescription: '所在地(鄉鎮)發布紅色燈號之天氣警特報',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/weather',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'weather_minor-general-v2',\n    channelName: '天氣警特報(一般)',\n    channelDescription: '所在地(鄉鎮)發布上述除外燈號之天氣警特報',\n    importance: NotificationImportance.Default,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/weather',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'evacuation_major-important-v2',\n    channelName: '防災資訊(重大)',\n    channelDescription: '所在地(鄉鎮)發布土石流、淹水或堰塞湖防災警訊時',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/warn',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_info',\n    channelKey: 'evacuation_minor-general-v2',\n    channelName: '防災資訊(一般)',\n    channelDescription: '所在地(鄉鎮)發布土石流、淹水或堰塞湖防災資訊時',\n    importance: NotificationImportance.High,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/normal',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: mediumVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_tsunami',\n    channelKey: 'tsunami-important-v2',\n    channelName: '海嘯資訊(重大)',\n    channelDescription: '海嘯警報發布時',\n    importance: NotificationImportance.Max,\n    defaultPrivacy: NotificationPrivacy.Public,\n    criticalAlerts: true,\n    playSound: true,\n    soundSource: 'resource://raw/tsunami',\n    defaultRingtoneType: DefaultRingtoneType.Alarm,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: highVibrationPattern,\n    locked: true,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_tsunami',\n    channelKey: 'tsunami-general-v2',\n    channelName: '海嘯資訊(一般)',\n    channelDescription: '海嘯消息發布時',\n    importance: NotificationImportance.Default,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/normal',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_tsunami',\n    channelKey: 'tsunami-silent-v2',\n    channelName: '太平洋海嘯消息 (無聲通知)',\n    channelDescription: '太平洋海嘯消息發布時',\n    groupAlertBehavior: GroupAlertBehavior.Children,\n    importance: NotificationImportance.Min,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: false,\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: false,\n  ),\n  NotificationChannel(\n    channelGroupKey: 'group_other',\n    channelKey: 'announcement-general-v2',\n    channelName: '其他通知',\n    channelDescription: '發送公告時',\n    importance: NotificationImportance.Default,\n    defaultPrivacy: NotificationPrivacy.Public,\n    playSound: true,\n    soundSource: 'resource://raw/info',\n    defaultColor: Colors.red,\n    ledColor: Colors.red,\n    enableVibration: true,\n    vibrationPattern: lowVibrationPattern,\n  ),\n  NotificationChannel(\n    channelKey: 'background',\n    channelName: '自動定位',\n    channelDescription: '背景定位服務通知',\n    importance: NotificationImportance.Low,\n    defaultColor: const Color(0xFF2196f3),\n    channelShowBadge: false,\n    enableVibration: false,\n    enableLights: false,\n    playSound: false,\n    locked: true,\n  ),\n];\n\nFuture<void> notifyInit() async {\n  final storedVersion = Global.preference.getInt(_channelVersionKey) ?? 0;\n  final needsForceUpdate = storedVersion < _notificationChannelVersion;\n\n  if (needsForceUpdate) {\n    TalkerManager.instance.info(\n      'Notification channel version changed: $storedVersion -> $_notificationChannelVersion, force updating channels',\n    );\n  }\n\n  await AwesomeNotifications().initialize(\n    'resource://drawable/ic_stat_name',\n    _notificationChannels,\n    channelGroups: [\n      NotificationChannelGroup(\n        channelGroupKey: 'group_eew',\n        channelGroupName: '地震速報',\n      ),\n      NotificationChannelGroup(\n        channelGroupKey: 'group_eq',\n        channelGroupName: '地震',\n      ),\n      NotificationChannelGroup(\n        channelGroupKey: 'group_info',\n        channelGroupName: '天氣',\n      ),\n      NotificationChannelGroup(\n        channelGroupKey: 'group_tsunami',\n        channelGroupName: '海嘯',\n      ),\n      NotificationChannelGroup(\n        channelGroupKey: 'group_other',\n        channelGroupName: '其他',\n      ),\n    ],\n    debug: true,\n  );\n\n  if (needsForceUpdate) {\n    for (var i = 0; i < _notificationChannels.length; i += 5) {\n      final batch = _notificationChannels.skip(i).take(5);\n      for (final channel in batch) {\n        try {\n          await AwesomeNotifications().setChannel(channel, forceUpdate: true);\n        } catch (e, st) {\n          TalkerManager.instance.error('setChannel failed: $channel', e, st);\n        }\n      }\n    }\n    await Global.preference.setInt(\n      _channelVersionKey,\n      _notificationChannelVersion,\n    );\n  }\n\n  AwesomeNotifications().setListeners(\n    onActionReceivedMethod: onActionReceivedMethod,\n  );\n}\n"
  },
  {
    "path": "lib/core/preference.dart",
    "content": "import 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/preference.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nclass PreferenceKeys {\n  static const lastUpdateToServerTime = 'lastUpdateToServerTime';\n  static const appVersion = 'app-version';\n\n  // #region Location\n  static const locationAuto = 'location:auto';\n  static const locationCode = 'location:code';\n  static const locationLongitude = 'location:longitude';\n  static const locationLatitude = 'location:latitude';\n  static const locationOldLongitude = 'location:oldLongitude';\n  static const locationOldLatitude = 'location:oldLatitude';\n  static const locationFavorited = 'location:favorite';\n  // #endregion\n\n  // #region User Interface\n  static const themeMode = 'pref:ui:mode';\n  static const themeColor = 'pref:ui:color';\n  static const locale = 'pref:ui:locale';\n  static const useFahrenheit = 'pref:ui:fahrenheit';\n  static const mapUpdateFps = 'pref:ui:map:updateFps';\n  static const mapBase = 'pref:ui:map:base';\n  static const mapLayers = 'pref:ui:map:layers';\n  static const mapAutoZoom = 'pref:ui:map:autoZoom';\n  static const homeDisplaySections = 'pref:ui:homeDisplaySections';\n\n  // #region Notification\n  static const notifyEew = 'pref:notify:eew';\n  static const notifyMonitor = 'pref:notify:monitor';\n  static const notifyReport = 'pref:notify:report';\n  static const notifyIntensity = 'pref:notify:intensity';\n  static const notifyThunderstorm = 'pref:notify:thunderstorm';\n  static const notifyWeatherAdvisory = 'pref:notify:weatherAdvisory';\n  static const notifyEvacuation = 'pref:notify:evacuation';\n  static const notifyTsunami = 'pref:notify:tsunami';\n  static const notifyAnnouncement = 'pref:notify:announcement';\n  // #endregion\n\n  // #region ETag Cache\n  static const weatherEtag = 'weather-etag';\n  static const weatherCache = 'weather-cache';\n  static const forecastEtag = 'forecast-etag';\n  static const forecastCache = 'forecast-cache';\n  static const radarListEtag = 'radar-list-etag';\n  static const radarListCache = 'radar-list-cache';\n  static const stationEtag = 'station-etag';\n  static const stationCache = 'station-cache';\n  static const realtimeListEtag = 'realtime-list-etag';\n  static const realtimeListCache = 'realtime-list-cache';\n  static const realtimeRegionEtag = 'realtime-region-etag';\n  static const realtimeRegionCache = 'realtime-region-cache';\n  static const historyListEtag = 'history-list-etag';\n  static const historyListCache = 'history-list-cache';\n  static const historyRegionEtag = 'history-region-etag';\n  static const historyRegionCache = 'history-region-cache';\n  // #endregion\n\n  // #region Network\n  static const proxyEnabled = 'network:proxy:enabled';\n  static const proxyHost = 'network:proxy:host';\n  static const proxyPort = 'network:proxy:port';\n  // #endregion\n\n  // #region Experimental\n  static const experimentalLaunchToMonitor = 'experimental:launchToMonitor';\n  static const experimentalEewAllSource = 'experimental:eewAllSource';\n  // #endregion\n}\n\nclass Preference {\n  Preference._();\n\n  static late SharedPreferencesWithCache instance;\n\n  static Future<void> init() async {\n    instance = await SharedPreferencesWithCache.create(\n      cacheOptions: const SharedPreferencesWithCacheOptions(),\n    );\n    AppLocalizations.locale = locale?.asLocale;\n  }\n\n  static Future<void> reload() async {\n    await instance.reloadCache();\n  }\n\n  static String? get version => instance.getString(PreferenceKeys.appVersion);\n  static set version(String? value) => instance.set(PreferenceKeys.appVersion, value);\n\n  static int? get lastUpdateToServerTime => instance.getInt(PreferenceKeys.lastUpdateToServerTime);\n  static set lastUpdateToServerTime(int? value) =>\n      instance.set(PreferenceKeys.lastUpdateToServerTime, value);\n\n  static bool get isTosAccepted => instance.getInt('accepted-tos-version') == 1;\n  static set isTosAccepted(bool value) => instance.set('accepted-tos-version', value ? 1 : null);\n\n  static bool get isFirstLaunch => instance.getString('welcome') != 'done';\n  static set isFirstLaunch(bool value) => instance.set('welcome', value ? null : 'done');\n\n  static String get notifyToken => instance.getString('notify-token') ?? '';\n  static set notifyToken(String? value) => instance.set('notify-token', value);\n\n  // #region Location\n  static bool? get locationAuto => instance.getBool(PreferenceKeys.locationAuto);\n  static set locationAuto(bool? value) => instance.set(PreferenceKeys.locationAuto, value);\n\n  static String? get locationCode => instance.getString(PreferenceKeys.locationCode);\n  static set locationCode(String? value) => instance.set(PreferenceKeys.locationCode, value);\n\n  static double? get locationLongitude => instance.getDouble(PreferenceKeys.locationLongitude);\n  static set locationLongitude(double? value) =>\n      instance.set(PreferenceKeys.locationLongitude, value);\n\n  static double? get locationLatitude => instance.getDouble(PreferenceKeys.locationLatitude);\n  static set locationLatitude(double? value) =>\n      instance.set(PreferenceKeys.locationLatitude, value);\n\n  static double? get locationOldLongitude =>\n      instance.getDouble(PreferenceKeys.locationOldLongitude);\n  static set locationOldLongitude(double? value) =>\n      instance.set(PreferenceKeys.locationOldLongitude, value);\n\n  static double? get locationOldLatitude => instance.getDouble(PreferenceKeys.locationOldLatitude);\n  static set locationOldLatitude(double? value) =>\n      instance.set(PreferenceKeys.locationOldLatitude, value);\n\n  static List<String> get locationFavorited =>\n      instance.getStringList(PreferenceKeys.locationFavorited) ?? [];\n  static set locationFavorited(List<String> value) =>\n      instance.set(PreferenceKeys.locationFavorited, value);\n  // #endregion\n\n  // #region User Interface\n  static String? get themeMode => instance.getString(PreferenceKeys.themeMode);\n  static set themeMode(String? value) => instance.set(PreferenceKeys.themeMode, value);\n\n  static int? get themeColor => instance.getInt(PreferenceKeys.themeColor);\n  static set themeColor(int? value) => instance.set(PreferenceKeys.themeColor, value);\n\n  static String? get locale => instance.getString(PreferenceKeys.locale);\n  static set locale(String? value) => instance.set(PreferenceKeys.locale, value);\n\n  static bool? get useFahrenheit => instance.getBool(PreferenceKeys.useFahrenheit);\n  static set useFahrenheit(bool? value) => instance.set(PreferenceKeys.useFahrenheit, value);\n\n  static int? get mapUpdateFps => instance.getInt(PreferenceKeys.mapUpdateFps);\n  static set mapUpdateFps(int? value) => instance.set(PreferenceKeys.mapUpdateFps, value);\n\n  static String? get mapBase => instance.getString(PreferenceKeys.mapBase);\n  static set mapBase(String? value) => instance.set(PreferenceKeys.mapBase, value);\n\n  static String? get mapLayers => instance.getString(PreferenceKeys.mapLayers);\n  static set mapLayers(String? value) => instance.set(PreferenceKeys.mapLayers, value);\n\n  static bool? get mapAutoZoom => instance.getBool(PreferenceKeys.mapAutoZoom);\n  static set mapAutoZoom(bool? value) => instance.set(PreferenceKeys.mapAutoZoom, value);\n\n  static List<String> get homeDisplaySections =>\n      instance.getStringList(PreferenceKeys.homeDisplaySections) ?? [];\n  static set homeDisplaySections(List<String> value) =>\n      instance.set(PreferenceKeys.homeDisplaySections, value);\n  // #endregion\n\n  // #region Notification\n  static String? get notifyEew => instance.getString(PreferenceKeys.notifyEew);\n  static set notifyEew(String? value) => instance.set(PreferenceKeys.notifyEew, value);\n\n  static String? get notifyMonitor => instance.getString(PreferenceKeys.notifyMonitor);\n  static set notifyMonitor(String? value) => instance.set(PreferenceKeys.notifyMonitor, value);\n\n  static String? get notifyReport => instance.getString(PreferenceKeys.notifyReport);\n  static set notifyReport(String? value) => instance.set(PreferenceKeys.notifyReport, value);\n\n  static String? get notifyIntensity => instance.getString(PreferenceKeys.notifyIntensity);\n  static set notifyIntensity(String? value) => instance.set(PreferenceKeys.notifyIntensity, value);\n\n  static String? get notifyThunderstorm => instance.getString(PreferenceKeys.notifyThunderstorm);\n  static set notifyThunderstorm(String? value) =>\n      instance.set(PreferenceKeys.notifyThunderstorm, value);\n\n  static String? get notifyWeatherAdvisory =>\n      instance.getString(PreferenceKeys.notifyWeatherAdvisory);\n  static set notifyWeatherAdvisory(String? value) =>\n      instance.set(PreferenceKeys.notifyWeatherAdvisory, value);\n\n  static String? get notifyEvacuation => instance.getString(PreferenceKeys.notifyEvacuation);\n  static set notifyEvacuation(String? value) =>\n      instance.set(PreferenceKeys.notifyEvacuation, value);\n\n  static String? get notifyTsunami => instance.getString(PreferenceKeys.notifyTsunami);\n  static set notifyTsunami(String? value) => instance.set(PreferenceKeys.notifyTsunami, value);\n\n  static String? get notifyAnnouncement => instance.getString(PreferenceKeys.notifyAnnouncement);\n  static set notifyAnnouncement(String? value) =>\n      instance.set(PreferenceKeys.notifyAnnouncement, value);\n  // #endregion\n\n  // #region Network\n  static bool? get proxyEnabled => instance.getBool(PreferenceKeys.proxyEnabled);\n  static set proxyEnabled(bool? value) => instance.set(PreferenceKeys.proxyEnabled, value);\n\n  static String? get proxyHost => instance.getString(PreferenceKeys.proxyHost);\n  static set proxyHost(String? value) => instance.set(PreferenceKeys.proxyHost, value);\n\n  static int? get proxyPort => instance.getInt(PreferenceKeys.proxyPort);\n  static set proxyPort(int? value) => instance.set(PreferenceKeys.proxyPort, value);\n  // #endregion\n\n  // #region Experimental\n  static bool? get experimentalLaunchToMonitor =>\n      instance.getBool(PreferenceKeys.experimentalLaunchToMonitor);\n  static set experimentalLaunchToMonitor(bool? value) =>\n      instance.set(PreferenceKeys.experimentalLaunchToMonitor, value);\n\n  static bool? get experimentalEewAllSource =>\n      instance.getBool(PreferenceKeys.experimentalEewAllSource);\n  static set experimentalEewAllSource(bool? value) =>\n      instance.set(PreferenceKeys.experimentalEewAllSource, value);\n  // #endregion\n}\n"
  },
  {
    "path": "lib/core/providers.dart",
    "content": "import 'package:dpip/models/data.dart';\nimport 'package:dpip/models/settings/location.dart';\nimport 'package:dpip/models/settings/map.dart';\nimport 'package:dpip/models/settings/notify.dart';\nimport 'package:dpip/models/settings/ui.dart';\n\nclass GlobalProviders {\n  GlobalProviders._();\n\n  static late DpipDataModel data;\n  static late SettingsLocationModel location;\n  static late SettingsMapModel map;\n  static late SettingsNotificationModel notification;\n  static late SettingsUserInterfaceModel ui;\n\n  static void init() {\n    data = DpipDataModel();\n    location = SettingsLocationModel();\n    map = SettingsMapModel();\n    notification = SettingsNotificationModel();\n    ui = SettingsUserInterfaceModel();\n  }\n}\n"
  },
  {
    "path": "lib/core/rts.dart",
    "content": "import 'package:dpip/api/model/station_info.dart';\nimport 'package:intl/intl.dart';\n\nStationInfo findAppropriateItem(List<StationInfo> infos, int date) {\n  final DateTime targetDate = (date == 0)\n      ? DateTime.now()\n      : DateTime.fromMillisecondsSinceEpoch(date);\n  final List<StationInfo> sortedItems = infos.toList()..sort((a, b) => a.time.compareTo(b.time));\n\n  for (var i = 0; i < sortedItems.length; i++) {\n    if (DateFormat(\n      'yyyy-MM-dd',\n    ).parse(sortedItems[i].time).isAfter(targetDate)) {\n      return i > 0 ? sortedItems[i - 1] : sortedItems[i];\n    }\n  }\n\n  return sortedItems.last;\n}\n"
  },
  {
    "path": "lib/core/service.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'dart:ui';\n\nimport 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';\nimport 'package:awesome_notifications/awesome_notifications.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/services.dart';\nimport 'package:geojson_vi/geojson_vi.dart';\nimport 'package:geolocator/geolocator.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\n\n/// Background location service with foreground support\nclass LocationServiceManager {\n  LocationServiceManager._();\n\n  static const int kAlarmId = 888888;\n  static const int kBackupAlarmId = 888890;\n  static const int kNotificationId = 888888;\n  static const String _kPrefKeyUpdateInterval = 'location_update_interval';\n\n  static const Duration kMinUpdateInterval = Duration(minutes: 5);\n  static const Duration kMaxUpdateInterval = Duration(minutes: 60);\n  static const Duration kDefaultUpdateInterval = Duration(minutes: 10);\n\n  static const double kHighMovementThreshold = 1000;\n  static const double kLowMovementThreshold = 100;\n\n  static const platform = MethodChannel('com.exptech.dpip/location');\n\n  static bool get available => Platform.isAndroid || Platform.isIOS;\n\n  static Future<void> initalize() async {\n    if (!Platform.isAndroid) return;\n\n    if (Preference.locationAuto != true) return;\n\n    final permission = await Geolocator.checkPermission();\n    if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever)\n      return;\n\n    try {\n      await stop();\n      await AndroidAlarmManager.initialize();\n      await LocationService._$task();\n      await start();\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        '👷 location service initialization failed',\n        e,\n        s,\n      );\n    }\n  }\n\n  static Duration _getUpdateInterval() {\n    final minutes = Preference.instance.getInt(_kPrefKeyUpdateInterval);\n    return minutes != null ? Duration(minutes: minutes) : kDefaultUpdateInterval;\n  }\n\n  static Future<void> _setUpdateInterval(Duration interval) async {\n    await Preference.instance.setInt(\n      _kPrefKeyUpdateInterval,\n      interval.inMinutes,\n    );\n  }\n\n  static Duration _calculateNextInterval(double? distanceInMeters) {\n    if (distanceInMeters == null) return kDefaultUpdateInterval;\n    if (distanceInMeters >= kHighMovementThreshold) return kMinUpdateInterval;\n    if (distanceInMeters >= kLowMovementThreshold) return kDefaultUpdateInterval;\n\n    final currentInterval = _getUpdateInterval();\n    final newInterval = Duration(minutes: currentInterval.inMinutes + 5);\n    return newInterval > kMaxUpdateInterval ? kMaxUpdateInterval : newInterval;\n  }\n\n  static Future<void> start() async {\n    if (!available) return;\n\n    try {\n      if (Platform.isIOS) {\n        await platform.invokeMethod('toggleLocation', {'isEnabled': true});\n        return;\n      }\n\n      await AndroidAlarmManager.cancel(kAlarmId);\n      await _setUpdateInterval(kDefaultUpdateInterval);\n\n      await AndroidAlarmManager.oneShot(\n        kDefaultUpdateInterval,\n        kAlarmId,\n        LocationService._$task,\n        wakeup: true,\n        exact: true,\n        rescheduleOnReboot: true,\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error('👷 starting location service FAILED', e, s);\n      if (e.toString().contains('SCHEDULE_EXACT_ALARM')) {\n        try {\n          await AndroidAlarmManager.oneShot(\n            kDefaultUpdateInterval,\n            kAlarmId,\n            LocationService._$task,\n            wakeup: true,\n            rescheduleOnReboot: true,\n          );\n        } catch (e2, s2) {\n          TalkerManager.instance.error(\n            '👷 starting inexact alarm also FAILED',\n            e2,\n            s2,\n          );\n        }\n      }\n    }\n\n    await AndroidAlarmManager.periodic(\n      Duration(hours: 1),\n      kBackupAlarmId,\n      LocationService._$task,\n      wakeup: true,\n      rescheduleOnReboot: true,\n    );\n  }\n\n  static Future<void> _rescheduleAlarm(Duration interval) async {\n    try {\n      await AndroidAlarmManager.cancel(kAlarmId);\n      await AndroidAlarmManager.oneShot(\n        interval,\n        kAlarmId,\n        LocationService._$task,\n        wakeup: true,\n        exact: true,\n        rescheduleOnReboot: true,\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error('👷 rescheduling alarm FAILED', e, s);\n    }\n  }\n\n  static Future<void> stop() async {\n    if (!available) return;\n    try {\n      if (Platform.isIOS) {\n        await platform.invokeMethod('toggleLocation', {'isEnabled': false});\n        return;\n      }\n\n      await AndroidAlarmManager.cancel(kAlarmId);\n      await AndroidAlarmManager.cancel(kBackupAlarmId);\n      await AwesomeNotifications().dismiss(kNotificationId);\n    } catch (e, s) {\n      TalkerManager.instance.error('👷 stopping location service FAILED', e, s);\n    }\n  }\n}\n\n@pragma('vm:entry-point')\nclass LocationService {\n  LocationService._();\n\n  static LatLng? _$location;\n  static GeoJSONFeatureCollection? _$geoJsonData;\n  static Map<String, Location>? _$locationData;\n\n  @pragma('vm:entry-point')\n  static Future<void> _$task() async {\n    try {\n      DartPluginRegistrant.ensureInitialized();\n      await Preference.init();\n      await AppLocalizations.load();\n      await LocationNameLocalizations.load();\n\n      try {\n        final _ = Global.packageInfo;\n      } catch (_) {\n        Global.packageInfo = await PackageInfo.fromPlatform();\n      }\n\n      if (Preference.locationAuto != true) {\n        await LocationServiceManager.stop();\n        return;\n      }\n\n      final permission = await Geolocator.checkPermission();\n      if (permission == LocationPermission.denied ||\n          permission == LocationPermission.deniedForever) {\n        TalkerManager.instance.warning(\n          '⚙️::BackgroundLocationService location permission not granted, stopping service',\n        );\n        await LocationServiceManager.stop();\n        return;\n      }\n\n      final isLocationEnabled = await Geolocator.isLocationServiceEnabled();\n      if (!isLocationEnabled) {\n        TalkerManager.instance.warning(\n          '⚙️::BackgroundLocationService location service is disabled, skipping this update',\n        );\n        await LocationServiceManager._rescheduleAlarm(\n          LocationServiceManager.kDefaultUpdateInterval,\n        );\n        return;\n      }\n\n      await _$showProcessingNotification();\n\n      _$geoJsonData ??= await Global.loadTownGeojson();\n      _$locationData ??= await Global.loadLocationData();\n      final coordinates = await _$getDeviceGeographicalLocation();\n\n      if (coordinates == null) {\n        await _$updatePosition(null);\n        await _$dismissNotification();\n        return;\n      }\n\n      final previousLocation = _$location;\n      final distanceInMeters = previousLocation != null ? coordinates.to(previousLocation) : null;\n      final nextInterval = LocationServiceManager._calculateNextInterval(\n        distanceInMeters,\n      );\n      await LocationServiceManager._setUpdateInterval(nextInterval);\n\n      await _$updatePosition(coordinates);\n\n      final fcmToken = Preference.notifyToken;\n      if (fcmToken.isNotEmpty) {\n        try {\n          await ExpTech().updateDeviceLocation(\n            token: fcmToken,\n            coordinates: coordinates,\n          );\n          TalkerManager.instance.info(\n            '⚙️::BackgroundLocationService location updated on server',\n          );\n        } catch (e, s) {\n          TalkerManager.instance.error(\n            '⚙️::BackgroundLocationService failed to update location on server',\n            e,\n            s,\n          );\n        }\n      }\n\n      await LocationServiceManager._rescheduleAlarm(nextInterval);\n\n      TalkerManager.instance.info(\n        '⚙️::BackgroundLocationService next update in ${nextInterval.inMinutes}min (distance: ${distanceInMeters?.toStringAsFixed(0) ?? \"unknown\"}m)',\n      );\n\n      await _$dismissNotification();\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        '⚙️::BackgroundLocationService task FAILED',\n        e,\n        s,\n      );\n      await _$dismissNotification();\n      try {\n        await LocationServiceManager._rescheduleAlarm(\n          LocationServiceManager.kDefaultUpdateInterval,\n        );\n      } catch (_) {}\n    }\n  }\n\n  @pragma('vm:entry-point')\n  static Future<void> _$showProcessingNotification() async {\n    try {\n      await AwesomeNotifications().createNotification(\n        content: NotificationContent(\n          id: LocationServiceManager.kNotificationId,\n          channelKey: 'background',\n          title: '正在更新位置'.i18n,\n          body: '取得 GPS 位置中...'.i18n,\n          icon: 'resource://drawable/ic_stat_name',\n          badge: 0,\n          category: NotificationCategory.Service,\n          notificationLayout: NotificationLayout.Default,\n        ),\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        '⚙️::BackgroundLocationService failed to show notification',\n        e,\n        s,\n      );\n    }\n  }\n\n  @pragma('vm:entry-point')\n  static Future<void> _$dismissNotification() async {\n    try {\n      await AwesomeNotifications().dismiss(\n        LocationServiceManager.kNotificationId,\n      );\n      await AwesomeNotifications().cancel(\n        LocationServiceManager.kNotificationId,\n      );\n      await AwesomeNotifications().dismissNotificationsByChannelKey(\n        'background',\n      );\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        '⚙️::BackgroundLocationService failed to dismiss notification',\n        e,\n        s,\n      );\n    }\n  }\n\n  @pragma('vm:entry-point')\n  static Future<LatLng?> _$getDeviceGeographicalLocation() async {\n    final permission = await Geolocator.checkPermission();\n    if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {\n      TalkerManager.instance.warning(\n        '⚙️::BackgroundLocationService location permission not granted',\n      );\n      return null;\n    }\n\n    final isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();\n    if (!isLocationServiceEnabled) {\n      TalkerManager.instance.warning(\n        '⚙️::BackgroundLocationService location service is not available',\n      );\n      return null;\n    }\n\n    try {\n      final lastKnown = await Geolocator.getLastKnownPosition();\n      if (lastKnown != null) {\n        final age = DateTime.now().difference(lastKnown.timestamp);\n        if (age.inMinutes < 10 && lastKnown.accuracy <= 500) {\n          return LatLng(lastKnown.latitude, lastKnown.longitude);\n        }\n      }\n    } catch (_) {}\n\n    try {\n      final lowAccuracyPosition = await Geolocator.getCurrentPosition(\n        locationSettings: const LocationSettings(\n          accuracy: LocationAccuracy.low,\n          timeLimit: Duration(seconds: 10),\n        ),\n      );\n      if (lowAccuracyPosition.accuracy <= 500) {\n        return LatLng(\n          lowAccuracyPosition.latitude,\n          lowAccuracyPosition.longitude,\n        );\n      }\n    } catch (_) {}\n\n    try {\n      final mediumAccuracyPosition = await Geolocator.getCurrentPosition(\n        locationSettings: const LocationSettings(\n          accuracy: LocationAccuracy.medium,\n          timeLimit: Duration(seconds: 15),\n        ),\n      );\n      return LatLng(\n        mediumAccuracyPosition.latitude,\n        mediumAccuracyPosition.longitude,\n      );\n    } catch (_) {}\n\n    try {\n      final currentPosition = await Geolocator.getCurrentPosition(\n        locationSettings: const LocationSettings(\n          accuracy: LocationAccuracy.high,\n          timeLimit: Duration(seconds: 30),\n        ),\n      );\n      return LatLng(currentPosition.latitude, currentPosition.longitude);\n    } catch (e) {\n      TalkerManager.instance.error(\n        '⚙️::BackgroundLocationService all location strategies failed',\n        e,\n      );\n      return null;\n    }\n  }\n\n  static ({String code, Location location})? _$getLocationFromCoordinates(\n    LatLng target,\n  ) {\n    final geoJsonData = _$geoJsonData;\n    final locationData = _$locationData;\n\n    if (geoJsonData == null || locationData == null) return null;\n\n    final features = geoJsonData.features;\n\n    for (final feature in features) {\n      if (feature == null) continue;\n      final geometry = feature.geometry;\n      if (geometry == null) continue;\n\n      bool isInPolygon = false;\n\n      if (geometry is GeoJSONPolygon) {\n        final polygon = geometry.coordinates[0];\n        bool isInside = false;\n        int j = polygon.length - 1;\n        for (int i = 0; i < polygon.length; i++) {\n          final double xi = polygon[i][0];\n          final double yi = polygon[i][1];\n          final double xj = polygon[j][0];\n          final double yj = polygon[j][1];\n          final bool intersect =\n              ((yi > target.latitude) != (yj > target.latitude)) &&\n              (target.longitude < (xj - xi) * (target.latitude - yi) / (yj - yi) + xi);\n          if (intersect) isInside = !isInside;\n          j = i;\n        }\n        isInPolygon = isInside;\n      }\n\n      if (geometry is GeoJSONMultiPolygon) {\n        final multiPolygon = geometry.coordinates;\n        for (final polygonCoordinates in multiPolygon) {\n          final polygon = polygonCoordinates[0];\n          bool isInside = false;\n          int j = polygon.length - 1;\n          for (int i = 0; i < polygon.length; i++) {\n            final double xi = polygon[i][0];\n            final double yi = polygon[i][1];\n            final double xj = polygon[j][0];\n            final double yj = polygon[j][1];\n            final bool intersect =\n                ((yi > target.latitude) != (yj > target.latitude)) &&\n                (target.longitude < (xj - xi) * (target.latitude - yi) / (yj - yi) + xi);\n            if (intersect) isInside = !isInside;\n            j = i;\n          }\n          if (isInside) {\n            isInPolygon = true;\n            break;\n          }\n        }\n      }\n\n      if (isInPolygon) {\n        final code = feature.properties!['CODE']?.toString();\n        if (code == null) return null;\n        final location = locationData[code];\n        if (location == null) return null;\n        return (code: code, location: location);\n      }\n    }\n\n    return null;\n  }\n\n  @pragma('vm:entry-point')\n  static Future<void> _$updatePosition(LatLng? position) async {\n    _$location = position;\n    final result = position != null ? _$getLocationFromCoordinates(position) : null;\n    Preference.locationCode = result?.code;\n    Preference.locationLatitude = position?.latitude;\n    Preference.locationLongitude = position?.longitude;\n  }\n}\n"
  },
  {
    "path": "lib/core/update.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nFuture<void> updateInfoToServer() async {\n  final latitude = Preference.locationLatitude;\n  final longitude = Preference.locationLongitude;\n\n  try {\n    if (latitude == null || longitude == null) return;\n    if (Preference.notifyToken != '' &&\n        DateTime.now().millisecondsSinceEpoch - (Preference.lastUpdateToServerTime ?? 0) >\n            86400 * 1 * 1000) {\n      final random = Random();\n      final int rand = random.nextInt(2);\n\n      if (rand != 0) return;\n\n      ExpTech().updateDeviceLocation(\n        token: Preference.notifyToken,\n        coordinates: LatLng(latitude, longitude),\n      );\n    }\n  } catch (e) {\n    print('Network info update failed: $e');\n  }\n}\n"
  },
  {
    "path": "lib/dialog/welcome/announcement.dart",
    "content": "import 'package:dpip/router.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass WelcomeAnnouncementDialog extends StatelessWidget {\n  const WelcomeAnnouncementDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      icon: const Icon(Symbols.announcement),\n      title: const Text('公告'),\n      content: const Text('有新的公告，要前往查看嗎？'),\n      actionsAlignment: MainAxisAlignment.spaceBetween,\n      actions: [\n        TextButton(\n          child: const Text('稍後再說'),\n          onPressed: () {\n            Navigator.pop(context);\n          },\n        ),\n        TextButton(\n          child: const Text('前往查看'),\n          onPressed: () {\n            Navigator.pop(context);\n            AnnouncementRoute().push(context);\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/dialog/welcome/changelog.dart",
    "content": "import 'package:dpip/router.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass WelcomeChangelogDialog extends StatelessWidget {\n  const WelcomeChangelogDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      icon: const Icon(Symbols.update_rounded),\n      title: const Text('更新完成'),\n      content: const Text('DPIP 更新完成，要前往查看更新日誌嗎？'),\n      actionsAlignment: MainAxisAlignment.spaceBetween,\n      actions: [\n        TextButton(\n          child: const Text('稍後再說'),\n          onPressed: () {\n            Navigator.pop(context);\n          },\n        ),\n        TextButton(\n          child: const Text('前往查看'),\n          onPressed: () {\n            Navigator.pop(context);\n            ChangelogRoute().push(context);\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/global.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/utils/extensions/asset_bundle.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/services.dart';\nimport 'package:geojson_vi/geojson_vi.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:zstandard/zstandard.dart';\n\ntypedef TimeTable = Map<String, List<({double P, double S, double R})>>;\n\nclass Global {\n  Global._();\n\n  static late PackageInfo packageInfo;\n  static late SharedPreferences preference;\n  static late Map<String, Location> location;\n  static late GeoJSONFeatureCollection boxGeojson;\n  static late GeoJSONFeatureCollection townGeojson;\n  static late TimeTable timeTable;\n  static late Map<String, ({String title, String body})> notifyTestContent;\n  static ExpTech api = ExpTech();\n\n  static Future<Map<String, dynamic>> _loadCompressedJson(\n    String assetPath,\n  ) async {\n    try {\n      final byteData = await rootBundle.load(assetPath);\n      final bytes = byteData.buffer.asUint8List();\n      late List<int> decompressed;\n\n      if (assetPath.endsWith('.zst')) {\n        final zstd = Zstandard();\n        final result = await zstd.decompress(bytes);\n        if (result == null) {\n          throw Exception('zstd decompress failed');\n        }\n        decompressed = result;\n      } else if (assetPath.endsWith('.gz')) {\n        decompressed = GZipCodec().decode(bytes);\n      } else {\n        decompressed = bytes;\n      }\n\n      final jsonString = utf8.decode(decompressed);\n      return jsonDecode(jsonString) as Map<String, dynamic>;\n    } catch (e, s) {\n      TalkerManager.instance.error(\n        'Global._loadCompressedJson($assetPath)',\n        e,\n        s,\n      );\n      return {};\n    }\n  }\n\n  static Future<Map<String, Location>> loadLocationData() async {\n    final data = await _loadCompressedJson('assets/location.json.gz');\n    return data.map(\n      (key, value) => MapEntry(key, Location.fromJson(value as Map<String, dynamic>)),\n    );\n  }\n\n  static Future<TimeTable> loadTimeTableData() async {\n    final data = await _loadCompressedJson('assets/time.json.gz');\n\n    return data.map((key, value) {\n      final list = (value as List).map((item) {\n        final map = item as Map<String, dynamic>;\n        return (\n          P: double.parse(map['P'].toString()),\n          R: double.parse(map['R'].toString()),\n          S: double.parse(map['S'].toString()),\n        );\n      }).toList();\n      return MapEntry(key, list);\n    });\n  }\n\n  static Future<void> loadNotifyTestContent() async {\n    final data = await rootBundle.loadJson('assets/notify_test.json');\n\n    notifyTestContent = data.map((type, value) {\n      final map = value as Map<String, dynamic>;\n      return MapEntry(type, (\n        title: map['title'].toString(),\n        body: map['body'].toString(),\n      ));\n    });\n  }\n\n  static Future<GeoJSONFeatureCollection> loadBoxGeojson() async {\n    final data = await rootBundle.loadJson('assets/box.json');\n\n    return GeoJSONFeatureCollection.fromMap(data);\n  }\n\n  static Future<GeoJSONFeatureCollection> loadTownGeojson() async {\n    final data = await _loadCompressedJson('assets/map/town.json.zst');\n\n    return GeoJSONFeatureCollection.fromMap(data);\n  }\n\n  static Future init() async {\n    final results = await Future.wait([\n      PackageInfo.fromPlatform(),\n      SharedPreferences.getInstance(),\n      loadBoxGeojson(),\n      loadLocationData(),\n      loadTimeTableData(),\n    ]);\n\n    packageInfo = (results[0] as PackageInfo?)!;\n    preference = (results[1] as SharedPreferences?)!;\n    boxGeojson = (results[2] as GeoJSONFeatureCollection?)!;\n    location = (results[3] as Map<String, Location>?)!;\n    timeTable = (results[4] as TimeTable?)!;\n\n    townGeojson = await loadTownGeojson();\n    await loadNotifyTestContent();\n  }\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:dpip/app.dart';\nimport 'package:dpip/core/compass.dart';\nimport 'package:dpip/core/device_info.dart';\nimport 'package:dpip/core/fcm.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/notify.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/core/service.dart';\nimport 'package:dpip/core/update.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:firebase_crashlytics/firebase_crashlytics.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';\nimport 'package:provider/provider.dart';\nimport 'package:timezone/data/latest.dart';\n\nfinal fcmReadyCompleter = Completer<void>();\nfinal talker = TalkerManager.instance;\nvoid main() async {\n  final overallStartTime = DateTime.now();\n  talker.log('--- 冷啟動偵測開始 ---');\n  talker.log('🔥 1. (main) 啟動時間: ${overallStartTime.toIso8601String()}');\n  WidgetsFlutterBinding.ensureInitialized();\n  String? initialShortcut;\n  if (Platform.isIOS) {\n    // iOS 14 以下改回用 StoreKit1\n    InAppPurchaseStoreKitPlatform.enableStoreKit1();\n  }\n\n  SystemChrome.setSystemUIOverlayStyle(\n    const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),\n  );\n  SystemChrome.setEnabledSystemUIMode(\n    SystemUiMode.edgeToEdge,\n    overlays: [SystemUiOverlay.top],\n  );\n\n  FlutterError.onError = (FlutterErrorDetails details) {\n    talker.handle(details.exception, details.stack);\n    if (Platform.isAndroid) {\n      FirebaseCrashlytics.instance.recordFlutterFatalError(details);\n    }\n  };\n\n  final globalInitStart = DateTime.now();\n  talker.log('⏳ 2. 啟動 Global...');\n  await Global.init();\n  final globalInitEnd = DateTime.now();\n  talker.log(\n    '✅ 2. Global 完成。耗時: ${globalInitEnd.difference(globalInitStart).inMilliseconds}ms',\n  );\n\n  await Preference.init();\n  final isFirstLaunch = Preference.instance.getBool('isFirstLaunch') ?? true;\n  GlobalProviders.init();\n  initializeTimeZones();\n  initialShortcut = await getInitialShortcut();\n\n  talker.log('⏳ 3. 啟動 並行任務... (測量總耗時)');\n  final futureWaitStart = DateTime.now();\n  await Future.wait([\n    _loggedTask('AppLocalizations.load', AppLocalizations.load()),\n    _loggedTask(\n      'LocationNameLocalizations.load',\n      LocationNameLocalizations.load(),\n    ),\n    // _loggedTask(\n    //   'WeatherStationLocalizations.load',\n    //   WeatherStationLocalizations.load(),\n    // ),\n  ]);\n  final futureWaitEnd = DateTime.now();\n  talker.log(\n    '✅ 3.並行任務全部完成。總耗時 (取決於最慢任務): ${futureWaitEnd.difference(futureWaitStart).inMilliseconds}ms',\n  );\n\n  if (Platform.isIOS) {\n    await DeviceInfo.init();\n  } else {\n    unawaited(\n      () async {\n        final start = DateTime.now();\n        await DeviceInfo.init();\n        talker.log(\n          '📱 DeviceInfo.init 完成 ${DateTime.now().difference(start).inMilliseconds}ms',\n        );\n      }(),\n    );\n  }\n\n  if (isFirstLaunch) {\n    talker.log('🟣 首次啟動 → 前置初始化 FCM + 通知');\n    await Future.wait([\n      _loggedTask('fcmInit', fcmInit()),\n      _loggedTask('notifyInit', notifyInit()),\n    ]);\n    unawaited(Future(() => updateInfoToServer()));\n    await Preference.instance.setBool('isFirstLaunch', false);\n  }\n\n  final overallEndTime = DateTime.now();\n  talker.log(\n    '🚨 總初始化耗時 (runApp 前): ${overallEndTime.difference(overallStartTime).inMilliseconds}ms',\n  );\n\n  runApp(\n    I18n(\n      initialLocale: GlobalProviders.ui.locale,\n      supportedLocales: [\n        'en'.asLocale,\n        'ja'.asLocale,\n        'ko'.asLocale,\n        'ru'.asLocale,\n        'vi'.asLocale,\n        'zh'.asLocale,\n        'zh-Hans'.asLocale,\n        'zh-Hant'.asLocale,\n      ],\n      localizationsDelegates: const [\n        GlobalMaterialLocalizations.delegate,\n        GlobalWidgetsLocalizations.delegate,\n        GlobalCupertinoLocalizations.delegate,\n      ],\n      child: MultiProvider(\n        providers: [\n          ChangeNotifierProvider.value(value: GlobalProviders.data),\n          ChangeNotifierProvider.value(value: GlobalProviders.location),\n          ChangeNotifierProvider.value(value: GlobalProviders.map),\n          ChangeNotifierProvider.value(value: GlobalProviders.notification),\n          ChangeNotifierProvider.value(value: GlobalProviders.ui),\n        ],\n        child: DpipApp(initialShortcut: initialShortcut),\n      ),\n    ),\n  );\n  if (!isFirstLaunch) {\n    talker.log('🟢 非首次啟動 → FCM + 通知 為背景初始化');\n    unawaited(\n      Future(() async {\n        try {\n          await fcmInit();\n          await notifyInit();\n          await updateInfoToServer();\n        } catch (e, st) {\n          talker.error('背景初始化失敗: $e\\n$st');\n        }\n      }),\n    );\n  }\n  unawaited(CompassService.instance.initialize());\n  final locationInitStart = DateTime.now();\n  talker.log('🚀 啟動 LocationServiceManager ...');\n  final locationFuture = LocationServiceManager.initalize();\n\n  locationFuture\n      .whenComplete(() {\n        final locationInitEnd = DateTime.now();\n        final locationDuration = locationInitEnd.difference(locationInitStart).inMilliseconds;\n        talker.log('✅ LocationServiceManager 完成。耗時: ${locationDuration}ms');\n      })\n      .catchError((e) {\n        talker.error('❌ LocationServiceManager 失敗。錯誤: $e');\n      });\n}\n\nconst platform = MethodChannel('com.exptech.dpip/shortcut');\n\nFuture<String?> getInitialShortcut() async {\n  try {\n    final result = await platform.invokeMethod<String>('getInitialShortcut');\n    return result;\n  } on PlatformException catch (e, st) {\n    talker.error('Failed to get initial shortcut', e, st);\n    return null;\n  }\n}\n\nFuture<T> _loggedTask<T>(String taskName, Future<T> future) async {\n  final start = DateTime.now();\n  try {\n    final result = await future;\n    final end = DateTime.now();\n    final duration = end.difference(start).inMilliseconds;\n    talker.log('  [並行] 任務 \"$taskName\" 完成。耗時: ${duration}ms');\n    return result;\n  } catch (e) {\n    final end = DateTime.now();\n    final duration = end.difference(start).inMilliseconds;\n    talker.error('  [並行] 任務 \"$taskName\" 失敗。耗時: ${duration}ms', e);\n    rethrow;\n  }\n}\n"
  },
  {
    "path": "lib/models/data.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\nimport 'dart:math';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/api/model/report/earthquake_report.dart';\nimport 'package:dpip/api/model/report/partial_earthquake_report.dart';\nimport 'package:dpip/api/model/rts/rts.dart';\nimport 'package:dpip/api/model/station.dart';\nimport 'package:dpip/api/model/weather/lightning.dart';\nimport 'package:dpip/api/model/weather/rain.dart';\nimport 'package:dpip/api/model/weather/weather.dart';\nimport 'package:dpip/core/eew.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/map_utils.dart';\nimport 'package:flutter/material.dart';\nimport 'package:geojson_vi/geojson_vi.dart';\n\nclass _DpipDataModel extends ChangeNotifier {\n  Map<String, Station> _station = {};\n\n  UnmodifiableMapView<String, Station> get station => UnmodifiableMapView(_station);\n\n  void setStation(Map<String, Station> station) {\n    _station = station;\n    notifyListeners();\n  }\n\n  Rts? _rts;\n\n  Rts? get rts => _rts;\n  int _rtsTime = 0;\n\n  List<Eew> _eew = [\n    // dummy data\n    /* Eew(\n      agency: 'cwa',\n      id: '1140907',\n      serial: 4,\n      status: 0,\n      isFinal: false,\n      info: EewInfo(\n        time: DateTime.now().millisecondsSinceEpoch,\n        longitude: 120.48,\n        latitude: 23.22,\n        depth: 10,\n        magnitude: 4.7,\n        location: '臺南市楠西區',\n        max: 4,\n      ),\n    ), */\n  ];\n\n  UnmodifiableListView<Eew> get eew => UnmodifiableListView(_eew);\n\n  int _eewHash = 0;\n\n  void setEew(List<Eew> eew) {\n    // Calculate hash from EEW id and serial\n    final newHash = Object.hashAll(eew.map((e) => Object.hash(e.id, e.serial)));\n\n    // Only notify if EEW data actually changed\n    if (_eewHash != newHash) {\n      _eewHash = newHash;\n      _eew = eew;\n      TalkerManager.instance.debug('[setEew] notify: hash=$newHash');\n      notifyListeners();\n    }\n  }\n\n  List<PartialEarthquakeReport> _partialReport = [];\n\n  UnmodifiableListView<PartialEarthquakeReport> get partialReport =>\n      UnmodifiableListView(_partialReport);\n\n  void setPartialReport(List<PartialEarthquakeReport> partialReport) {\n    _partialReport = partialReport;\n    notifyListeners();\n  }\n\n  void appendPartialReport(List<PartialEarthquakeReport> partialReport) {\n    final existingIds = _partialReport.map((r) => r.id).toSet();\n    final newReports = partialReport.where((r) => !existingIds.contains(r.id)).toList();\n    if (newReports.isNotEmpty) {\n      _partialReport = [..._partialReport, ...newReports];\n      notifyListeners();\n    }\n  }\n\n  Map<String, EarthquakeReport> _report = {};\n\n  UnmodifiableMapView<String, EarthquakeReport> get report => UnmodifiableMapView(_report);\n\n  void setReport(String id, EarthquakeReport report) {\n    _report[id] = report;\n    notifyListeners();\n  }\n\n  void setReports(Map<String, EarthquakeReport> report) {\n    _report = report;\n    notifyListeners();\n  }\n\n  List<String> _radar = [];\n\n  UnmodifiableListView<String> get radar => UnmodifiableListView(_radar);\n\n  void setRadar(List<String> radar) {\n    _radar = radar;\n    notifyListeners();\n  }\n\n  List<String> _temperature = [];\n\n  UnmodifiableListView<String> get temperature => UnmodifiableListView(_temperature);\n\n  void setTemperature(List<String> temperature) {\n    _temperature = temperature;\n    notifyListeners();\n  }\n\n  final Map<String, List<WeatherStation>> _weatherData = {};\n\n  UnmodifiableMapView<String, List<WeatherStation>> get weatherData =>\n      UnmodifiableMapView(_weatherData);\n\n  void setWeatherData(String time, List<WeatherStation> weather) {\n    _weatherData[time] = weather;\n    notifyListeners();\n  }\n\n  List<String> _precipitation = [];\n\n  UnmodifiableListView<String> get precipitation => UnmodifiableListView(_precipitation);\n\n  void setPrecipitation(List<String> precipitation) {\n    _precipitation = precipitation;\n    notifyListeners();\n  }\n\n  final Map<String, List<RainStation>> _rainData = {};\n\n  UnmodifiableMapView<String, List<RainStation>> get rainData => UnmodifiableMapView(_rainData);\n\n  void setRainData(String time, List<RainStation> rain) {\n    _rainData[time] = rain;\n    notifyListeners();\n  }\n\n  List<String> _wind = [];\n\n  UnmodifiableListView<String> get wind => UnmodifiableListView(_wind);\n\n  void setWind(List<String> wind) {\n    _wind = wind;\n    notifyListeners();\n  }\n\n  List<String> _lightning = [];\n\n  UnmodifiableListView<String> get lightning => UnmodifiableListView(_lightning);\n\n  void setLightning(List<String> lightning) {\n    _lightning = lightning;\n    notifyListeners();\n  }\n\n  final Map<String, List<Lightning>> _lightningData = {};\n\n  UnmodifiableMapView<String, List<Lightning>> get lightningData =>\n      UnmodifiableMapView(_lightningData);\n\n  void setLightningData(String time, List<Lightning> lightning) {\n    _lightningData[time] = lightning;\n    notifyListeners();\n  }\n\n  int _timeOffset = 0;\n\n  int get timeOffset => _timeOffset;\n\n  void setTimeOffset(int timeOffset) {\n    _timeOffset = timeOffset;\n    notifyListeners();\n  }\n}\n\nclass DpipDataModel extends _DpipDataModel {\n  static const int _eewActiveWindow = 4 * 60 * 1000; // 4 minutes in milliseconds\n  static const double _rtsCoordinateOffset = 0.00009; // ~5m displacement for privacy\n\n  Timer? _secondTimer;\n  Timer? _minuteTimer;\n  bool _isInForeground = true;\n  bool _isReplayMode = false;\n  int? _replayTimestamp;\n  int? _replayStartTime;\n  final Random _random = Random();\n  int? _syncDistance;\n  int _lastSyncTime = 0;\n\n  int get currentTime {\n    final now = DateTime.now().millisecondsSinceEpoch;\n    return _isReplayMode && _replayTimestamp != null && _replayStartTime != null\n        ? _replayTimestamp! + (now - _replayStartTime!)\n        : now + timeOffset;\n  }\n\n  UnmodifiableListView<Eew> get activeEew {\n    final cutoffTime = currentTime - _eewActiveWindow;\n    return UnmodifiableListView(\n      _eew.where((eew) => eew.info.time >= cutoffTime).toList(),\n    );\n  }\n\n  void setRts(Rts rts) {\n    final incoming = rts.time;\n    if (!_isReplayMode && incoming <= _rtsTime) return;\n    _rtsTime = incoming;\n    _rts = rts;\n    notifyListeners();\n  }\n\n  int? get syncTime {\n    if (_rts != null) return _rts!.time;\n    if (_isReplayMode) return currentTime;\n\n    return null;\n  }\n\n  void setReplayMode(bool isReplay, [int? timestamp]) {\n    _isReplayMode = isReplay;\n    if (isReplay) {\n      if (timestamp == null) {\n        throw ArgumentError('Timestamp must be provided in replay mode');\n      }\n      _replayTimestamp = timestamp;\n      _replayStartTime = DateTime.now().millisecondsSinceEpoch;\n      _rtsTime = timestamp - 1;\n    } else {\n      _replayTimestamp = null;\n      _replayStartTime = null;\n      _rtsTime = 0;\n    }\n    notifyListeners();\n  }\n\n  void _updateRtsData(Rts? rts, List<Eew> eew) {\n    if (rts != null) {\n      _syncDistance = currentTime - rts.time;\n      setRts(rts);\n    }\n    setEew(eew);\n  }\n\n  Future<(Rts?, List<Eew>)> _fetchRtsData() async {\n    Rts? rts;\n    List<Eew> eew = [];\n\n    try {\n      rts = _isReplayMode ? await ExpTech().getRts(currentTime) : await ExpTech().getRts();\n    } on Rtsnodata {\n      rts = null;\n    }\n    try {\n      eew = _isReplayMode ? await ExpTech().getEew(currentTime) : await ExpTech().getEew();\n    } catch (e) {\n      eew = [];\n    }\n\n    return (rts, eew);\n  }\n\n  Future<void> fetchRtsImmediately() async {\n    if (!_isInForeground) return;\n    try {\n      final (rts, eew) = await _fetchRtsData();\n      _updateRtsData(rts, eew);\n    } catch (e, s) {\n      TalkerManager.instance.error('fetchRtsImmediately', e, s);\n    }\n  }\n\n  void startFetching() {\n    if (_secondTimer != null) return;\n\n    Future<void> fetchCallback() async {\n      if (!_isInForeground) return;\n      try {\n        final (rts, eew) = await _fetchRtsData();\n        _updateRtsData(rts, eew);\n      } catch (e, s) {\n        TalkerManager.instance.error('fetchCallback', e, s);\n      }\n    }\n\n    _secondTimer = Timer.periodic(const Duration(seconds: 1), (_) async {\n      if (!_isInForeground) return;\n\n      final now = DateTime.now().millisecondsSinceEpoch;\n      final timeSinceLastSync = now - _lastSyncTime;\n      final shouldSync = timeSinceLastSync >= 10000;\n\n      await fetchCallback();\n\n      if (shouldSync && _syncDistance != null) {\n        _lastSyncTime = now;\n\n        final serverMs = (currentTime % 1000 - _syncDistance! % 1000 + 1000) % 1000;\n        final delayToNextSecond = serverMs == 0 ? 1000 : (1000 - serverMs);\n\n        Timer(Duration(milliseconds: delayToNextSecond), () async {\n          if (!_isInForeground) return;\n          try {\n            final (rts, eew) = await _fetchRtsData();\n            _updateRtsData(rts, eew);\n          } catch (e, s) {\n            TalkerManager.instance.error('syncCallback', e, s);\n          }\n        });\n      }\n    });\n\n    _lastSyncTime = DateTime.now().millisecondsSinceEpoch;\n    fetchCallback();\n\n    Future<void> everyMinuteCallback() async {\n      if (!_isInForeground) return;\n\n      try {\n        final data = await Future.wait(\n          [ExpTech().getNtp(), ExpTech().getStations()],\n          cleanUp: (successValue) {\n            switch (successValue) {\n              case int():\n                setTimeOffset(\n                  successValue - DateTime.now().millisecondsSinceEpoch,\n                );\n              case Map<String, Station>():\n                setStation(successValue);\n            }\n          },\n        );\n\n        final [ntp as int, stations as Map<String, Station>] = data;\n        setTimeOffset(ntp - DateTime.now().millisecondsSinceEpoch);\n        setStation(stations);\n      } catch (e, s) {\n        TalkerManager.instance.error('everyMinuteCallback', e, s);\n      }\n    }\n\n    everyMinuteCallback();\n    _minuteTimer = Timer.periodic(\n      const Duration(minutes: 1),\n      (_) => everyMinuteCallback(),\n    );\n  }\n\n  void stopFetching() {\n    _secondTimer?.cancel();\n    _secondTimer = null;\n    _minuteTimer?.cancel();\n    _minuteTimer = null;\n    _lastSyncTime = 0;\n  }\n\n  int? get lastRtsPing => _syncDistance;\n\n  void onAppLifecycleStateChanged(AppLifecycleState state) {\n    _isInForeground = state == AppLifecycleState.resumed;\n    if (_isInForeground) {\n      startFetching();\n    } else {\n      stopFetching();\n    }\n  }\n\n  @override\n  void dispose() {\n    stopFetching();\n    super.dispose();\n  }\n\n  Map<String, dynamic> getRtsGeoJson() {\n    final rts = this.rts;\n    final builder = GeoJsonBuilder();\n\n    for (final MapEntry(key: id, value: s) in station.entries) {\n      if (!s.work) continue;\n\n      final baseCoordinates = s.info.last.latlng.asGeoJsonCooridnate;\n      final offsetLng = (_random.nextDouble() - 0.5) * _rtsCoordinateOffset;\n      final offsetLat = (_random.nextDouble() - 0.5) * _rtsCoordinateOffset;\n      final displacedCoordinates = [\n        baseCoordinates[0] + offsetLng,\n        baseCoordinates[1] + offsetLat,\n      ];\n\n      final feature = GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n        ..setGeometry(displacedCoordinates)\n        ..setId(int.parse(id))\n        ..setProperty('id', id)\n        ..setProperty('net', s.net)\n        ..setProperty('code', s.info.last.code);\n\n      if (rts != null) {\n        final data = rts.station[id];\n        if (data != null) {\n          feature\n            ..setProperty('i', data.i)\n            ..setProperty('I', data.I)\n            ..setProperty('pga', data.pga)\n            ..setProperty('pgv', data.pgv);\n        }\n      }\n\n      final location = Global.location['${s.info.last.code}'];\n      if (location != null) {\n        feature\n          ..setProperty('city', location.cityWithLevel)\n          ..setProperty('town', location.townWithLevel);\n      }\n\n      builder.addFeature(feature);\n    }\n\n    return builder.build();\n  }\n\n  Map<String, dynamic> getEewGeoJson() {\n    final eew = activeEew;\n    if (eew.isEmpty) return GeoJsonBuilder().build();\n\n    final builder = GeoJsonBuilder();\n    final now = currentTime;\n\n    for (final e in eew) {\n      final radius = calcWaveRadius(e.info.depth, e.info.time, now);\n      final center = e.info.latlng;\n\n      if (radius.p > 0) {\n        builder.addFeature(\n          circleFeature(center: center, radius: radius.p)..setProperty('type', 'p'),\n        );\n      }\n\n      if (radius.s > 0) {\n        builder.addFeature(\n          circleFeature(center: center, radius: radius.s)..setProperty('type', 's'),\n        );\n      }\n\n      builder.addFeature(\n        GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n          ..setGeometry(center.asGeoJsonCooridnate)\n          ..setProperty('type', 'x'),\n      );\n    }\n\n    return builder.build();\n  }\n\n  Map<String, dynamic> getIntensityGeoJson() {\n    final rts = this.rts;\n    final builder = GeoJsonBuilder();\n\n    for (final MapEntry(key: id, value: s) in station.entries) {\n      if (!s.work) continue;\n\n      final feature = GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n        ..setGeometry(s.info.last.latlng.asGeoJsonCooridnate)\n        ..setId(int.parse(id))\n        ..setProperty('net', s.net)\n        ..setProperty('code', s.info.last.code);\n\n      if (rts != null) {\n        final data = rts.station[id];\n        if (data != null) {\n          final isAlert = data.alert ?? false;\n          feature\n            ..setProperty(\n              'intensity',\n              intensityFloatToInt(isAlert ? data.I : data.i),\n            )\n            ..setProperty('alert', isAlert ? 1 : 0);\n        }\n      }\n\n      builder.addFeature(feature);\n    }\n\n    return builder.build();\n  }\n\n  Map<String, dynamic> getBoxGeoJson() {\n    final rts = this.rts;\n    if (rts == null || rts.box.isEmpty) return GeoJsonBuilder().build();\n\n    final builder = GeoJsonBuilder();\n    final now = currentTime;\n    final activeEew = eew;\n\n    // Precompute EEW data if needed\n    Map<String, Eew>? eewMap;\n    Map<String, double>? eewDistMap;\n    if (activeEew.isNotEmpty) {\n      eewMap = {for (final e in activeEew) e.id: e};\n      eewDistMap = {\n        for (final e in activeEew) e.id: calcWaveRadius(e.info.depth, e.info.time, now).s * 1000,\n      };\n    }\n\n    for (final area in Global.boxGeojson.features) {\n      if (area == null) continue;\n\n      final id = area.properties!['ID'].toString();\n      if (!rts.box.containsKey(id)) continue;\n\n      final coordinates = (area.geometry! as GeoJSONPolygon).coordinates[0];\n\n      if (eewMap != null && eewDistMap != null && checkBoxSkip(eewMap, eewDistMap, coordinates)) {\n        continue;\n      }\n\n      builder.addFeature(\n        GeoJsonFeatureBuilder(GeoJsonFeatureType.Polygon)\n          ..setGeometry(coordinates)\n          ..setProperty('i', rts.box[id]),\n      );\n    }\n\n    return builder.build();\n  }\n}\n"
  },
  {
    "path": "lib/models/map/earthquake.dart",
    "content": "import 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/api/model/rts/rts.dart';\nimport 'package:flutter/material.dart';\n\nclass MapEarthquakeModel extends ChangeNotifier {\n  final rts = ValueNotifier<Rts?>(null);\n  final eew = <String, Eew?>{};\n\n  void setRts(Rts? rts) {\n    this.rts.value = rts;\n    notifyListeners();\n  }\n\n  void setEew(String id, Eew? eew) {\n    this.eew[id] = eew;\n    notifyListeners();\n  }\n}\n"
  },
  {
    "path": "lib/models/settings/location.dart",
    "content": "import 'dart:collection';\n\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/global.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:provider/provider.dart';\n\nclass _SettingsLocationModel extends ChangeNotifier {\n  /// The underlying [ValueNotifier] for the current location represented as a postal code.\n  ///\n  /// Returns the stored location code from preferences. Returns `null` if no location code has been set.\n  final $code = ValueNotifier(Preference.locationCode);\n\n  /// The current location represented as a postal code.\n  ///\n  /// Returns the stored location code from preferences. Returns `null` if no location code has been set.\n  String? get code => $code.value;\n\n  /// Sets the current location using a postal code.\n  ///\n  /// [value] The postal code to set as the current location.\n  ///\n  /// Invoking this method will also update [$code] and notify all attached listeners.\n  ///\n  /// If [value] matches the current code, no changes are made. When [auto] is false, also updates the stored latitude\n  /// and longitude based on the location data associated with the postal code.\n  void setCode(String? value) {\n    if (code == value) return;\n\n    final location = Global.location[value];\n\n    // Check if the location is invalid\n    if (location == null) {\n      Preference.locationCode = null;\n      $code.value = null;\n\n      if (!auto) {\n        Preference.locationLatitude = null;\n        Preference.locationLongitude = null;\n\n        $coordinates.value = null;\n      }\n\n      notifyListeners();\n      return;\n    }\n\n    Preference.locationCode = value;\n    $code.value = value;\n\n    if (!auto) {\n      Preference.locationLatitude = location.lat;\n      Preference.locationLongitude = location.lng;\n\n      $coordinates.value = LatLng(location.lat, location.lng);\n    }\n\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the current location represented as a [LatLng] coordinate.\n  ///\n  /// Returns a [LatLng] object containing the stored coordinates for the current [code]. Returns `null` if either\n  /// latitude or longitude is not set.\n  ///\n  /// This is used to display the precise location of the user on the map.\n  ///\n  /// Depends on [code].\n  final $coordinates = ValueNotifier(\n    Preference.locationLatitude != null && Preference.locationLongitude != null\n        ? LatLng(Preference.locationLatitude!, Preference.locationLongitude!)\n        : null,\n  );\n\n  /// The current location represented as a LatLng coordinate.\n  ///\n  /// Returns a [LatLng] object containing the stored coordinates for the current [code]. Returns `null` if either\n  /// latitude or longitude is not set.\n  ///\n  /// This is used to display the precise location of the user on the map.\n  ///\n  /// Depends on [code].\n  LatLng? get coordinates => $coordinates.value;\n\n  /// Sets the current location using a LatLng coordinate.\n  ///\n  /// Takes a [LatLng] value containing latitude and longitude coordinates and updates the stored location preferences.\n  /// If value is `null`, both latitude and longitude will be set to `null`.\n  ///\n  /// Invoking this method will also update [$coordinates] and notify all attached listeners.\n  ///\n  /// This method should be called aside with [setCode] if automatic location update is enabled.\n  ///\n  /// Use [setCode] instead when automatic location update is disabled.\n  void setCoordinates(LatLng? value) {\n    Preference.locationLatitude = value?.latitude;\n    Preference.locationLongitude = value?.longitude;\n\n    $coordinates.value = value;\n\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the current state of automatic location update.\n  ///\n  /// Returns a [bool] indicating if automatic location update is enabled. When enabled, the app will use GPS to\n  /// automatically update the current location. When disabled, the location must be set manually either by [setCode] or\n  /// [setCoordinates].\n  ///\n  /// Defaults to `false` if no preference has been set.\n  final $auto = ValueNotifier(Preference.locationAuto ?? false);\n\n  /// The current state of automatic location update.\n  ///\n  /// Returns a [bool] indicating if automatic location update is enabled. When enabled, the app will use GPS to\n  /// automatically update the current location. When disabled, the location must be set manually either by [setCode] or\n  /// [setCoordinates].\n  ///\n  /// Defaults to `false` if no preference has been set.\n  bool get auto => $auto.value;\n\n  /// Sets whether location should be automatically determined using GPS.\n  ///\n  /// Takes a [bool] value indicating if automatic location detection should be enabled. When enabled, the app will use\n  /// GPS to automatically determine and update the current location. When disabled, the location must be set manually.\n  void setAuto(bool value) {\n    Preference.locationAuto = value;\n\n    $auto.value = value;\n\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the list of favorited locations.\n  ///\n  /// Returns a [List] of [String] containing the postal codes of the favorited locations.\n  ///\n  /// Defaults to an empty list if no favorited locations have been set.\n  final $favorited = ValueNotifier(Preference.locationFavorited.toSet());\n\n  /// The list of favorited locations.\n  ///\n  /// Returns a [UnmodifiableSetView<String>] containing the postal codes of the favorited locations.\n  ///\n  /// Defaults to an empty [Set] if no favorited locations have been set.\n  UnmodifiableSetView<String> get favorited => UnmodifiableSetView($favorited.value);\n\n  /// Adds a location to the list of favorited locations.\n  ///\n  /// Takes a [String] value representing the postal code of the location to add to the list.\n  ///\n  /// If the location is already favorited, this method will do nothing.\n  void favorite(String code) {\n    final list = {...Preference.locationFavorited}..add(code);\n\n    Preference.locationFavorited = list.toList();\n\n    $favorited.value = list;\n\n    notifyListeners();\n  }\n\n  /// Removes a location from the list of favorited locations.\n  ///\n  /// Takes a [String] value representing the postal code of the location to remove from the list.\n  ///\n  /// If the location is not favorited, this method will do nothing.\n  void unfavorite(String code) {\n    final list = {...Preference.locationFavorited}..remove(code);\n\n    Preference.locationFavorited = list.toList();\n\n    $favorited.value = list;\n\n    notifyListeners();\n  }\n\n  /// Check if the provided location is favorited\n  bool isFavorited(String code) => favorited.contains(code);\n\n  /// Refreshes the location settings from preferences.\n  ///\n  /// Updates the [code], [coordinates], and [auto] properties to reflect the current preferences.\n  ///\n  /// This method is used to refresh the location settings when the preferences are updated.\n  void refresh() {\n    $code.value = Preference.locationCode;\n    $coordinates.value = Preference.locationLatitude != null && Preference.locationLongitude != null\n        ? LatLng(Preference.locationLatitude!, Preference.locationLongitude!)\n        : null;\n    $auto.value = Preference.locationAuto ?? false;\n    $favorited.value = Preference.locationFavorited.toSet();\n    notifyListeners();\n  }\n}\n\nclass SettingsLocationModel extends _SettingsLocationModel {}\n\nextension SettingsLocationModelExtension on BuildContext {\n  SettingsLocationModel get useLocation => watch<SettingsLocationModel>();\n  SettingsLocationModel get location => read<SettingsLocationModel>();\n}\n"
  },
  {
    "path": "lib/models/settings/map.dart",
    "content": "import 'dart:collection';\n\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/utils/extensions/iterable.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass _SettingsMapModel extends ChangeNotifier {\n  void _log(String message) => TalkerManager.instance.info('[SettingsMapModel] $message');\n\n  /// The underlying [ValueNotifier] for the map update interval in frames per second.\n  ///\n  /// Returns the stored FPS value from preferences. Defaults to `10` if no\n  /// value has been set.\n  final updateIntervalNotifier = ValueNotifier(Preference.mapUpdateFps ?? 10);\n\n  /// The current map update interval in frames per second.\n  ///\n  /// Returns the stored FPS value from preferences. Defaults to `10` if no\n  /// value has been set.\n  int get updateInterval => updateIntervalNotifier.value;\n\n  /// Sets the map update interval in frames per second.\n  ///\n  /// Persists [value] to preferences, updates [updateIntervalNotifier], and\n  /// notifies all attached listeners.\n  void setUpdateInterval(int value) {\n    Preference.mapUpdateFps = value;\n    updateIntervalNotifier.value = value;\n    _log(\n      'Changed ${PreferenceKeys.mapUpdateFps} to ${Preference.mapUpdateFps}',\n    );\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the active base map type.\n  ///\n  /// Returns the stored [BaseMapType] from preferences. Defaults to\n  /// [BaseMapType.exptech] if no value has been set.\n  final baseMapNotifier = ValueNotifier(\n    BaseMapType.values.asNameMap()[Preference.mapBase] ?? BaseMapType.exptech,\n  );\n\n  /// The active base map type.\n  ///\n  /// Returns the stored [BaseMapType] from preferences. Defaults to\n  /// [BaseMapType.exptech] if no value has been set.\n  BaseMapType get baseMap => baseMapNotifier.value;\n\n  /// Sets the active base map type.\n  ///\n  /// Persists [value] to preferences, updates [baseMapNotifier], and notifies\n  /// all attached listeners.\n  void setBaseMapType(BaseMapType value) {\n    Preference.mapBase = value.name;\n    baseMapNotifier.value = value;\n    _log('Changed ${PreferenceKeys.mapBase} to $value');\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the set of active map layers.\n  ///\n  /// Returns the stored [Set<MapLayer>] from preferences. Defaults to\n  /// `{MapLayer.monitor}` if no value has been set.\n  final layersNotifier = ValueNotifier(\n    Preference.mapLayers?.split(',').map((v) => MapLayer.values.byName(v)).toSet() ??\n        {MapLayer.monitor},\n  );\n\n  /// The set of active map layers, ordered by [MapLayer.values].\n  ///\n  /// Returns an [UnmodifiableSetView<MapLayer>] from preferences. Defaults to\n  /// `{MapLayer.monitor}` if no value has been set.\n  UnmodifiableSetView<MapLayer> get layers =>\n      UnmodifiableSetView(layersNotifier.value.orderedBy(MapLayer.values));\n\n  /// Sets the active map layers.\n  ///\n  /// Sorts [value] by [MapLayer.values] order, persists it to preferences,\n  /// updates [layersNotifier], and notifies all attached listeners.\n  void setLayers(Set<MapLayer> value) {\n    final sorted = value.orderedBy(MapLayer.values);\n    Preference.mapLayers = sorted.map((e) => e.name).join(',');\n    layersNotifier.value = sorted;\n    _log('Changed ${PreferenceKeys.mapLayers} to $value');\n    notifyListeners();\n  }\n\n  /// The underlying [ValueNotifier] for the auto-zoom setting.\n  ///\n  /// Returns a [bool] indicating whether the map should automatically zoom to\n  /// relevant events. Defaults to `false` if no value has been set.\n  final autoZoomNotifier = ValueNotifier(Preference.mapAutoZoom ?? false);\n\n  /// Whether the map automatically zooms to relevant events.\n  ///\n  /// Returns a [bool] from preferences. Defaults to `false` if no value has\n  /// been set.\n  bool get autoZoom => autoZoomNotifier.value;\n\n  /// Sets whether the map should automatically zoom to relevant events.\n  ///\n  /// Persists [value] to preferences, updates [autoZoomNotifier], and notifies\n  /// all attached listeners.\n  void setAutoZoom(bool value) {\n    Preference.mapAutoZoom = value;\n    autoZoomNotifier.value = value;\n    _log('Changed ${PreferenceKeys.mapAutoZoom} to $value');\n    notifyListeners();\n  }\n\n  /// Refreshes the map settings from preferences.\n  ///\n  /// Updates [updateInterval], [baseMap], [layers], and [autoZoom] to reflect\n  /// the current preferences, then notifies all attached listeners.\n  void refresh() {\n    updateIntervalNotifier.value = Preference.mapUpdateFps ?? 10;\n    baseMapNotifier.value =\n        BaseMapType.values.asNameMap()[Preference.mapBase] ?? BaseMapType.exptech;\n    layersNotifier.value =\n        Preference.mapLayers?.split(',').map((v) => MapLayer.values.byName(v)).toSet() ??\n        {MapLayer.monitor};\n    autoZoomNotifier.value = Preference.mapAutoZoom ?? false;\n  }\n}\n\nclass SettingsMapModel extends _SettingsMapModel {}\n\nextension SettingsMapModelExtension on BuildContext {\n  /// Watches [SettingsMapModel] and rebuilds when it notifies listeners.\n  SettingsMapModel get useMap => watch<SettingsMapModel>();\n\n  /// Reads [SettingsMapModel] without subscribing to updates.\n  SettingsMapModel get map => read<SettingsMapModel>();\n}\n"
  },
  {
    "path": "lib/models/settings/notify.dart",
    "content": "import 'dart:developer';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/notify/notify_settings.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// Notification channel identifiers used when updating server-side settings.\nenum NotifyChannel {\n  /// Earthquake early warning.\n  eew,\n\n  /// Seismic intensity monitor.\n  monitor,\n\n  /// Earthquake report.\n  report,\n\n  /// Intensity report.\n  intensity,\n\n  /// Real-time thunderstorm alerts.\n  thunderstorm,\n\n  /// Weather advisory alerts.\n  weatherAdvisory,\n\n  /// Disaster evacuation alerts.\n  evacuation,\n\n  /// Tsunami alerts.\n  tsunami,\n\n  /// General announcements.\n  announcement,\n}\n\n/// Notification filter for earthquake early warning alerts.\nenum EewNotifyType {\n  /// Local intensity 4 or above.\n  localIntensityAbove4,\n\n  /// Local intensity 1 or above.\n  localIntensityAbove1,\n\n  /// All warnings.\n  all,\n}\n\n/// Notification filter for earthquake events.\nenum EarthquakeNotifyType {\n  /// Disabled.\n  off,\n\n  /// Local intensity 1 or above.\n  localIntensityAbove1,\n\n  /// All events.\n  all,\n}\n\n/// Notification filter for weather events.\nenum WeatherNotifyType {\n  /// Disabled.\n  off,\n\n  /// Local area only.\n  local,\n}\n\n/// Notification filter for tsunami alerts.\nenum TsunamiNotifyType {\n  /// Tsunami warnings only.\n  warningOnly,\n\n  /// All tsunami alerts.\n  all,\n}\n\n/// Notification filter for general announcements.\nenum BasicNotifyType {\n  /// Disabled.\n  off,\n\n  /// All announcements.\n  all,\n}\n\nclass _SettingsNotificationModel extends ChangeNotifier {\n  void _log(String message) => log(message, name: 'SettingsNotificationModel');\n\n  String get _eew => Preference.notifyEew ?? EewNotifyType.localIntensityAbove1.name;\n\n  String get _monitor => Preference.notifyMonitor ?? EarthquakeNotifyType.localIntensityAbove1.name;\n\n  String get _report => Preference.notifyReport ?? EarthquakeNotifyType.localIntensityAbove1.name;\n\n  String get _intensity =>\n      Preference.notifyIntensity ?? EarthquakeNotifyType.localIntensityAbove1.name;\n\n  String get _thunderstorm => Preference.notifyThunderstorm ?? WeatherNotifyType.local.name;\n\n  String get _weatherAdvisory => Preference.notifyWeatherAdvisory ?? WeatherNotifyType.local.name;\n\n  String get _evacuation => Preference.notifyEvacuation ?? WeatherNotifyType.local.name;\n\n  String get _tsunami => Preference.notifyTsunami ?? TsunamiNotifyType.all.name;\n\n  String get _announcement => Preference.notifyAnnouncement ?? BasicNotifyType.all.name;\n\n  /// Applies notification settings received from the server.\n  ///\n  /// Overwrites all channel preferences with the values in [settings] and\n  /// notifies all attached listeners.\n  void apply(NotifySettings settings) {\n    Preference.notifyEew = settings.eew.name;\n    Preference.notifyMonitor = settings.monitor.name;\n    Preference.notifyReport = settings.report.name;\n    Preference.notifyIntensity = settings.intensity.name;\n    Preference.notifyThunderstorm = settings.thunderstorm.name;\n    Preference.notifyWeatherAdvisory = settings.weatherAdvisory.name;\n    Preference.notifyEvacuation = settings.evacuation.name;\n    Preference.notifyTsunami = settings.tsunami.name;\n    Preference.notifyAnnouncement = settings.announcement.name;\n\n    _log('Applied notification settings from server');\n    notifyListeners();\n  }\n\n  /// The current earthquake early warning notification filter.\n  ///\n  /// Returns an [EewNotifyType] from preferences. Defaults to\n  /// [EewNotifyType.localIntensityAbove1] if no value has been set.\n  EewNotifyType get eew => EewNotifyType.values.byName(_eew);\n\n  /// Sets the earthquake early warning notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setEew(EewNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.eew,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyEew = value.name;\n    _log('Changed ${PreferenceKeys.notifyEew} to ${Preference.notifyEew}');\n    notifyListeners();\n  }\n\n  /// The current seismic intensity monitor notification filter.\n  ///\n  /// Returns an [EarthquakeNotifyType] from preferences. Defaults to\n  /// [EarthquakeNotifyType.localIntensityAbove1] if no value has been set.\n  EarthquakeNotifyType get monitor => EarthquakeNotifyType.values.byName(_monitor);\n\n  /// Sets the seismic intensity monitor notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setMonitor(EarthquakeNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.monitor,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyMonitor = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyMonitor} to ${Preference.notifyMonitor}',\n    );\n    notifyListeners();\n  }\n\n  /// The current earthquake report notification filter.\n  ///\n  /// Returns an [EarthquakeNotifyType] from preferences. Defaults to\n  /// [EarthquakeNotifyType.localIntensityAbove1] if no value has been set.\n  EarthquakeNotifyType get report => EarthquakeNotifyType.values.byName(_report);\n\n  /// Sets the earthquake report notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setReport(EarthquakeNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.report,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyReport = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyReport} to ${Preference.notifyReport}',\n    );\n    notifyListeners();\n  }\n\n  /// The current intensity report notification filter.\n  ///\n  /// Returns an [EarthquakeNotifyType] from preferences. Defaults to\n  /// [EarthquakeNotifyType.localIntensityAbove1] if no value has been set.\n  EarthquakeNotifyType get intensity => EarthquakeNotifyType.values.byName(_intensity);\n\n  /// Sets the intensity report notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setIntensity(EarthquakeNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.intensity,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyIntensity = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyIntensity} to ${Preference.notifyIntensity}',\n    );\n    notifyListeners();\n  }\n\n  /// The current thunderstorm alert notification filter.\n  ///\n  /// Returns a [WeatherNotifyType] from preferences. Defaults to\n  /// [WeatherNotifyType.local] if no value has been set.\n  WeatherNotifyType get thunderstorm => WeatherNotifyType.values.byName(_thunderstorm);\n\n  /// Sets the thunderstorm alert notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setThunderstorm(WeatherNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.thunderstorm,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyThunderstorm = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyThunderstorm} to ${Preference.notifyThunderstorm}',\n    );\n    notifyListeners();\n  }\n\n  /// The current weather advisory notification filter.\n  ///\n  /// Returns a [WeatherNotifyType] from preferences. Defaults to\n  /// [WeatherNotifyType.local] if no value has been set.\n  WeatherNotifyType get weatherAdvisory => WeatherNotifyType.values.byName(_weatherAdvisory);\n\n  /// Sets the weather advisory notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setWeatherAdvisory(WeatherNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.weatherAdvisory,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyWeatherAdvisory = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyWeatherAdvisory} to ${Preference.notifyWeatherAdvisory}',\n    );\n    notifyListeners();\n  }\n\n  /// The current disaster evacuation notification filter.\n  ///\n  /// Returns a [WeatherNotifyType] from preferences. Defaults to\n  /// [WeatherNotifyType.local] if no value has been set.\n  WeatherNotifyType get evacuation => WeatherNotifyType.values.byName(_evacuation);\n\n  /// Sets the disaster evacuation notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setEvacuation(WeatherNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.evacuation,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyEvacuation = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyEvacuation} to ${Preference.notifyEvacuation}',\n    );\n    notifyListeners();\n  }\n\n  /// The current tsunami alert notification filter.\n  ///\n  /// Returns a [TsunamiNotifyType] from preferences. Defaults to\n  /// [TsunamiNotifyType.all] if no value has been set.\n  TsunamiNotifyType get tsunami => TsunamiNotifyType.values.byName(_tsunami);\n\n  /// Sets the tsunami alert notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setTsunami(TsunamiNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.tsunami,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyTsunami = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyTsunami} to ${Preference.notifyTsunami}',\n    );\n    notifyListeners();\n  }\n\n  /// The current announcement notification filter.\n  ///\n  /// Returns a [BasicNotifyType] from preferences. Defaults to\n  /// [BasicNotifyType.all] if no value has been set.\n  BasicNotifyType get announcement => BasicNotifyType.values.byName(_announcement);\n\n  /// Sets the announcement notification filter.\n  ///\n  /// Sends [value] to the server, applies the returned settings, persists the\n  /// value to preferences, and notifies all attached listeners.\n  Future<void> setAnnouncement(BasicNotifyType value) async {\n    final result = await ExpTech().setNotify(\n      token: Preference.notifyToken,\n      channel: NotifyChannel.announcement,\n      status: value,\n    );\n    GlobalProviders.notification.apply(result);\n\n    Preference.notifyAnnouncement = value.name;\n    _log(\n      'Changed ${PreferenceKeys.notifyAnnouncement} to ${Preference.notifyAnnouncement}',\n    );\n    notifyListeners();\n  }\n}\n\nclass SettingsNotificationModel extends _SettingsNotificationModel {}\n\nextension SettingsNotificationModelExtension on BuildContext {\n  /// Watches [SettingsNotificationModel] and rebuilds when it notifies listeners.\n  SettingsNotificationModel get useMap => watch<SettingsNotificationModel>();\n\n  /// Reads [SettingsNotificationModel] without subscribing to updates.\n  SettingsNotificationModel get map => read<SettingsNotificationModel>();\n}\n"
  },
  {
    "path": "lib/models/settings/ui.dart",
    "content": "import 'dart:developer';\nimport 'dart:io';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nenum HomeDisplaySection {\n  radar,\n  forecast,\n  wind,\n}\n\nconst reorderableSections = [\n  HomeDisplaySection.radar,\n  HomeDisplaySection.forecast,\n  HomeDisplaySection.wind,\n];\n\nclass SettingsUserInterfaceModel extends ChangeNotifier {\n  void _log(String message) => log(message, name: 'SettingsUserInterfaceModel');\n\n  late String _currentThemeMode;\n  late int? _currentThemeColor;\n  late String? _currentLocale;\n  late bool _currentUseFahrenheit;\n  late List<HomeDisplaySection> homeSections;\n\n  SettingsUserInterfaceModel() {\n    _currentThemeMode = Preference.themeMode ?? 'system';\n    _currentThemeColor = Preference.themeColor;\n    _currentLocale = Preference.locale;\n    _currentUseFahrenheit = Preference.useFahrenheit ?? false;\n\n    _log('Initialized: themeMode=$_currentThemeMode');\n\n    final savedList = Preference.homeDisplaySections;\n    if (savedList.isEmpty) {\n      homeSections = HomeDisplaySection.values.toList();\n    } else {\n      final saved = savedList\n          .map(\n            (s) => HomeDisplaySection.values.cast<HomeDisplaySection?>().firstWhere(\n              (e) => e?.name == s,\n              orElse: () => null,\n            ),\n          )\n          .whereType<HomeDisplaySection>()\n          .toList();\n      homeSections = saved;\n    }\n  }\n\n  ThemeMode get themeMode => ThemeMode.values.byName(_currentThemeMode);\n  void setThemeMode(ThemeMode value) {\n    _currentThemeMode = value.name;\n    Preference.themeMode = value.name;\n\n    _log('Changed themeMode to $_currentThemeMode');\n    notifyListeners();\n  }\n\n  Color? get themeColor => _currentThemeColor == null ? null : Color(_currentThemeColor!);\n  void setThemeColor(Color? color) {\n    final colorInt = color?.toARGB32();\n    _currentThemeColor = colorInt;\n    Preference.themeColor = colorInt;\n\n    _log('Changed themeColor to $_currentThemeColor');\n    notifyListeners();\n  }\n\n  Locale? get locale => _currentLocale?.asLocale;\n  void setLocale(Locale? value) {\n    _currentLocale = value?.toLanguageTag();\n    Preference.locale = _currentLocale;\n    AppLocalizations.locale = value ?? Platform.localeName.asLocale;\n\n    _log('Changed locale to $_currentLocale');\n    notifyListeners();\n  }\n\n  bool get useFahrenheit => _currentUseFahrenheit;\n  void setUseFahrenheit(bool value) {\n    _currentUseFahrenheit = value;\n    Preference.useFahrenheit = value;\n\n    _log('Changed useFahrenheit to $_currentUseFahrenheit');\n    notifyListeners();\n  }\n\n  bool isEnabled(HomeDisplaySection section) => homeSections.contains(section);\n\n  void toggleSection(HomeDisplaySection section, bool enabled) {\n    if (enabled) {\n      homeSections.add(section);\n    } else {\n      homeSections.remove(section);\n    }\n    _saveHomeSections();\n    notifyListeners();\n  }\n\n  void reorderSection(int oldIndex, int newIndex) {\n    if (oldIndex < newIndex) {\n      newIndex -= 1;\n    }\n    final item = homeSections.removeAt(oldIndex);\n    homeSections.insert(newIndex, item);\n    _saveHomeSections();\n    notifyListeners();\n  }\n\n  void _saveHomeSections() {\n    Preference.homeDisplaySections = homeSections.map((e) => e.name).toList();\n  }\n}\n\nextension SettingsUserInterfaceModelExtension on BuildContext {\n  SettingsUserInterfaceModel get useUserInterface => watch<SettingsUserInterfaceModel>();\n  SettingsUserInterfaceModel get userInterface => read<SettingsUserInterfaceModel>();\n}\n"
  },
  {
    "path": "lib/route/announcement/announcement.dart",
    "content": "import 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/announcement.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_markdown/flutter_markdown.dart';\nimport 'package:intl/intl.dart';\n\nfinal List<TagType> tagTypes = [\n  TagType(id: 0, text: '錯誤'.i18n, color: Colors.red),\n  TagType(id: 1, text: '已解決'.i18n, color: Colors.green),\n  TagType(id: 2, text: '影響：小'.i18n, color: Colors.grey.shade600),\n  TagType(id: 3, text: '影響：中'.i18n, color: Colors.orange.shade700),\n  TagType(id: 4, text: '影響：大'.i18n, color: Colors.purple),\n  TagType(id: 5, text: '公告'.i18n, color: Colors.blue.shade900),\n  TagType(id: 6, text: '維修'.i18n, color: Colors.teal.shade700),\n  TagType(id: 7, text: '測試'.i18n, color: Colors.cyan.shade700),\n  TagType(id: 8, text: '變更'.i18n, color: Colors.pink.shade600),\n  TagType(id: 9, text: '完成'.i18n, color: Colors.lightGreen.shade700),\n  TagType(id: 10, text: '地震相關'.i18n, color: Colors.deepOrange.shade600),\n  TagType(id: 11, text: '氣象相關'.i18n, color: Colors.indigo.shade600),\n];\n\nTagType _getTagTypeById(int id) {\n  return tagTypes.firstWhere(\n    (tagType) => tagType.id == id,\n    orElse: () => TagType(id: -1, text: '未知'.i18n, color: Colors.grey),\n  );\n}\n\nString _formatDate(int timestamp) {\n  final date = DateTime.fromMillisecondsSinceEpoch(timestamp);\n  return DateFormat('yyyy/MM/dd HH:mm').format(date);\n}\n\nclass TagType {\n  final int id;\n  final String text;\n  final Color color;\n\n  TagType({required this.id, required this.text, required this.color});\n}\n\nclass AnnouncementPage extends StatefulWidget {\n  const AnnouncementPage({super.key});\n\n  @override\n  _AnnouncementPageState createState() => _AnnouncementPageState();\n}\n\nclass _AnnouncementPageState extends State<AnnouncementPage> {\n  List<Announcement> announcements = [];\n  bool isLoading = true;\n  String? errorMessage;\n\n  @override\n  void initState() {\n    super.initState();\n    _fetchAnnouncements();\n  }\n\n  Future<void> _fetchAnnouncements() async {\n    try {\n      final fetchedAnnouncements = (await ExpTech().getAnnouncement()).reversed.toList();\n      setState(() {\n        announcements = fetchedAnnouncements;\n        isLoading = false;\n      });\n    } catch (e) {\n      setState(() {\n        errorMessage = '取得公告時發生錯誤: $e'.i18n;\n        isLoading = false;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('公告'.i18n), elevation: 0),\n      body: SafeArea(\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const SizedBox(height: 16),\n              Expanded(child: _buildAnnouncementList()),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildAnnouncementList() {\n    if (isLoading) {\n      return const Center(child: CircularProgressIndicator());\n    }\n    if (errorMessage != null) {\n      return Center(child: Text(errorMessage!));\n    }\n    if (announcements.isEmpty) {\n      return Center(child: Text('目前沒有公告'.i18n));\n    }\n    return RefreshIndicator(\n      onRefresh: _fetchAnnouncements,\n      child: ListView.builder(\n        itemCount: announcements.length,\n        itemBuilder: (context, index) {\n          return AnnouncementCard(\n            announcement: announcements[index],\n            tagTypes: tagTypes,\n            onTap: () {\n              Navigator.push(\n                context,\n                MaterialPageRoute(\n                  builder: (context) => AnnouncementDetailPage(\n                    announcement: announcements[index],\n                    tagTypes: tagTypes,\n                  ),\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass AnnouncementCard extends StatelessWidget {\n  final Announcement announcement;\n  final List<TagType> tagTypes;\n  final VoidCallback onTap;\n\n  const AnnouncementCard({\n    super.key,\n    required this.announcement,\n    required this.tagTypes,\n    required this.onTap,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 4,\n      margin: const EdgeInsets.only(bottom: 16),\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),\n      child: InkWell(\n        onTap: onTap,\n        borderRadius: BorderRadius.circular(16),\n        child: Padding(\n          padding: const EdgeInsets.all(16.0),\n          child: Row(\n            children: [\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      announcement.title,\n                      style: context.theme.textTheme.titleMedium?.copyWith(\n                        fontWeight: FontWeight.bold,\n                      ),\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const SizedBox(height: 4),\n                    _buildDateChip(context),\n                    const SizedBox(height: 8),\n                    _buildTags(context),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 16),\n              Icon(\n                Icons.arrow_forward_ios,\n                size: 16,\n                color: context.theme.textTheme.bodySmall?.color?.withValues(\n                  alpha: 0.5,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDateChip(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Icon(Icons.access_time, size: 12, color: context.colors.primary),\n        const SizedBox(width: 4),\n        Text(\n          _formatDate(announcement.time),\n          style: context.theme.textTheme.bodySmall?.copyWith(\n            color: context.colors.primary,\n            fontWeight: FontWeight.bold,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildTags(BuildContext context) {\n    return Wrap(\n      spacing: 4,\n      runSpacing: 4,\n      children: announcement.tags\n          .map((tagId) => _buildGlassyTag(context, _getTagTypeById(tagId)))\n          .toList(),\n    );\n  }\n\n  Widget _buildGlassyTag(BuildContext context, TagType tagType) {\n    return Chip(\n      padding: EdgeInsets.zero,\n      side: BorderSide(color: tagType.color),\n      backgroundColor: tagType.color.withValues(alpha: 0.16),\n      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n      label: Text(\n        tagType.text,\n        style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),\n      ),\n    );\n  }\n}\n\nclass AnnouncementDetailPage extends StatelessWidget {\n  final Announcement announcement;\n  final List<TagType> tagTypes;\n\n  const AnnouncementDetailPage({\n    super.key,\n    required this.announcement,\n    required this.tagTypes,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('公告詳情'.i18n), elevation: 0),\n      body: SafeArea(\n        child: SingleChildScrollView(\n          padding: const EdgeInsets.all(16.0),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text(\n                announcement.title,\n                style: context.theme.textTheme.headlineSmall?.copyWith(\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              const SizedBox(height: 16),\n              _buildDateChip(context),\n              const SizedBox(height: 16),\n              Wrap(\n                spacing: 8,\n                runSpacing: 8,\n                children: announcement.tags\n                    .map(\n                      (tagId) => _buildGlassyTag(context, _getTagTypeById(tagId)),\n                    )\n                    .toList(),\n              ),\n              const SizedBox(height: 24),\n              MarkdownBody(\n                data: announcement.content,\n                styleSheet: MarkdownStyleSheet.fromTheme(context.theme).copyWith(\n                  h1: context.theme.textTheme.headlineMedium?.copyWith(\n                    fontWeight: FontWeight.bold,\n                  ),\n                  h2: context.theme.textTheme.headlineSmall?.copyWith(\n                    fontWeight: FontWeight.bold,\n                  ),\n                  p: context.theme.textTheme.bodyLarge,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDateChip(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n      decoration: BoxDecoration(\n        color: context.colors.primary.withValues(alpha: 0.1),\n        borderRadius: BorderRadius.circular(20),\n        border: Border.all(\n          color: context.colors.primary.withValues(alpha: 0.3),\n        ),\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Icon(Icons.calendar_today, size: 18, color: context.colors.primary),\n          const SizedBox(width: 8),\n          Text(\n            _formatDate(announcement.time),\n            style: context.theme.textTheme.bodyMedium?.copyWith(\n              color: context.colors.primary,\n              fontWeight: FontWeight.bold,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildGlassyTag(BuildContext context, TagType tagType) {\n    return Chip(\n      padding: EdgeInsets.zero,\n      side: BorderSide(color: tagType.color),\n      backgroundColor: tagType.color.withValues(alpha: 0.16),\n      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n      label: Text(\n        tagType.text,\n        style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/event_viewer/intensity.dart",
    "content": "import 'dart:async';\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/history/intensity_history.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/utils/list_icon.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:dpip/widgets/chip/label_chip.dart';\nimport 'package:dpip/widgets/list/detail_field_tile.dart';\nimport 'package:dpip/widgets/map/legend.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/bottom_sheet_drag_handle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:timezone/timezone.dart';\n\nclass IntensityPage extends StatefulWidget {\n  final IntensityHistory item;\n\n  const IntensityPage({super.key, required this.item});\n\n  @override\n  _IntensityPageState createState() => _IntensityPageState();\n}\n\nclass _IntensityPageState extends State<IntensityPage> {\n  late MapLibreMapController _mapController;\n  List<String> radarList = [];\n  double userLat = 0;\n  double userLon = 0;\n  bool isUserLocationValid = false;\n  bool _showLegend = false;\n  Timer? _update;\n  late IntensityHistory data = widget.item;\n\n  @override\n  void dispose() {\n    _mapController.dispose();\n    _update?.cancel();\n    super.dispose();\n  }\n\n  void _initMap(MapLibreMapController controller) {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    radarList = await ExpTech().getRadarList();\n\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    }\n\n    await _mapController.addSource(\n      'markers-geojson',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n\n    start();\n  }\n\n  Future<void> start() async {\n    final location = GlobalProviders.location.coordinates;\n\n    if (location != null && location.isValid) {\n      await _mapController.animateCamera(\n        CameraUpdate.newLatLngZoom(location, 7.4),\n      );\n    } else {\n      await _mapController.animateCamera(\n        CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n      );\n    }\n\n    getEventInfo();\n\n    if (!widget.item.addition.isFinal) {\n      _update = Timer.periodic(const Duration(seconds: 1), (_) async {\n        data = (await ExpTech().getEvent(widget.item.id))[0] as IntensityHistory;\n        getEventInfo();\n        if (data.addition.isFinal == true) {\n          _update?.cancel();\n        }\n      });\n    }\n  }\n\n  Future<void> getEventInfo() async {\n    final originalArea = data.addition.area;\n    final Map<String, int> invertedArea = {};\n\n    originalArea.forEach((key, value) {\n      for (final code in value) {\n        invertedArea[code.toString()] = int.parse(key);\n      }\n    });\n\n    await _mapController.setLayerProperties(\n      'town',\n      FillLayerProperties(\n        fillColor: [\n          'match',\n          ['get', 'CODE'],\n          ...invertedArea.entries.expand(\n            (entry) => [\n              int.parse(entry.key),\n              IntensityColor.intensity(entry.value).toHexStringRGB(),\n            ],\n          ),\n          context.colors.surfaceContainerHighest.toHexStringRGB(),\n        ],\n        fillOpacity: 1,\n      ),\n    );\n  }\n\n  void _toggleLegend() {\n    setState(() {\n      _showLegend = !_showLegend;\n    });\n  }\n\n  Widget _buildLegend() {\n    return MapLegend(\n      label: 'TREM 觀測網實測震度',\n      children: [\n        _buildColorBar(),\n        const SizedBox(height: 8),\n        _buildColorBarLabels(),\n        Text(\n          '使用 JMA 震度標準 (0.3秒三分量合成加速度)',\n          style: context.theme.textTheme.labelMedium,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildColorBar() {\n    final intensities = [1, 2, 3, 4, 5, 6, 7, 8, 9];\n    return SizedBox(\n      height: 20,\n      width: 300,\n      child: Row(\n        children: intensities.map((intensity) {\n          return Expanded(\n            child: Container(color: IntensityColor.intensity(intensity)),\n          );\n        }).toList(),\n      ),\n    );\n  }\n\n  Widget _buildColorBarLabels() {\n    final labels = List.generate(9, (i) {\n      final count = i + 1;\n      const map = {5: '5弱', 6: '5強', 7: '6弱', 8: '6強', 9: '7級'};\n      return map[count] ?? '$count級';\n    });\n\n    return SizedBox(\n      width: 300,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: labels.map((label) {\n          return SizedBox(\n            width: 300 / 9,\n            child: Text(\n              label,\n              style: const TextStyle(fontSize: 10),\n              textAlign: TextAlign.center,\n            ),\n          );\n        }).toList(),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.item.text.content['all']?.title ?? ''),\n        elevation: 0,\n      ),\n      body: Stack(\n        children: [\n          DpipMap(onMapCreated: _initMap, onStyleLoadedCallback: _loadMap),\n          Positioned(\n            right: 4,\n            top: 4,\n            child: Material(\n              color: context.colors.secondary,\n              elevation: 4.0,\n              shape: const CircleBorder(),\n              clipBehavior: Clip.antiAlias,\n              child: InkWell(\n                onTap: _toggleLegend,\n                child: Tooltip(\n                  message: '圖例',\n                  child: Container(\n                    width: 30,\n                    height: 30,\n                    alignment: Alignment.center,\n                    child: Icon(\n                      _showLegend ? Icons.close : Icons.info_outline,\n                      size: 20,\n                      color: context.colors.onSecondary,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          if (_showLegend)\n            Positioned(\n              right: 6,\n              top: 50, // Adjusted to be above the legend button\n              child: _buildLegend(),\n            ),\n          _buildDraggableSheet(context),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildDraggableSheet(BuildContext context) {\n    return DraggableScrollableSheet(\n      initialChildSize: 0.35,\n      minChildSize: 0.15,\n      snap: true,\n      snapSizes: const [0.15, 0.35, 1],\n      builder: (context, scrollController) {\n        return Container(\n          decoration: BoxDecoration(\n            color: context.colors.surfaceContainer,\n            borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),\n            boxShadow: kElevationToShadow[4],\n          ),\n          child: ListView(\n            controller: scrollController,\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n            children: [\n              const BottomSheetDragHandle(),\n              _buildWarningHeader(),\n              const Divider(),\n              _buildWarningDetails(),\n              Padding(\n                padding: const EdgeInsets.fromLTRB(10, 4, 0, 0),\n                child: Text(\n                  '本資料係由 TREM-Net 觀測網自動觀測結果所得，尚未經人為檢視確認，僅供應變之初步參考。實際應以中央氣象署發布之資訊為準。',\n                  style: TextStyle(color: context.colors.error),\n                ),\n              ),\n              const SizedBox(height: 20),\n              _buildAffectedAreas(),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildWarningHeader() {\n    final String subtitle = widget.item.text.content['all']?.subtitle ?? '';\n\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.all(8),\n            decoration: BoxDecoration(\n              color: context.colors.secondaryContainer,\n              borderRadius: BorderRadius.circular(12),\n            ),\n            child: Icon(getListIcon(widget.item.icon), size: 28),\n          ),\n          const SizedBox(width: 12),\n          Row(\n            children: [\n              Text(\n                subtitle,\n                style: context.theme.textTheme.titleLarge?.copyWith(\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              const SizedBox(width: 8),\n              LabelChip(\n                label: \"第${data.addition.serial}報${(data.addition.isFinal) ? '(最終)' : \"\"}\",\n                backgroundColor: context.colors.secondaryContainer,\n                foregroundColor: context.colors.onSecondaryContainer,\n                outlineColor: context.colors.secondaryContainer,\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildWarningDetails() {\n    final DateTime sendTime = widget.item.time.send;\n    final int expireTimestamp = widget.item.time.expires['all']!;\n    final TZDateTime expireTimeUTC = parseDateTime(expireTimestamp);\n    final String description = data.text.description['all'] ?? '';\n    final bool isExpired = TZDateTime.now(UTC).isAfter(expireTimeUTC.toUtc());\n    final DateTime localExpireTime = expireTimeUTC;\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Container(\n          margin: const EdgeInsets.symmetric(vertical: 8),\n          padding: const EdgeInsets.symmetric(horizontal: 8),\n          child: Text(description, style: context.theme.textTheme.bodyLarge),\n        ),\n        _buildTimeBar(context, sendTime, localExpireTime, isExpired),\n      ],\n    );\n  }\n\n  Widget _buildTimeBar(\n    BuildContext context,\n    DateTime sendTime,\n    DateTime expireTime,\n    bool isExpired,\n  ) {\n    return Container(\n      margin: const EdgeInsets.symmetric(vertical: 8),\n      padding: const EdgeInsets.all(16),\n      decoration: BoxDecoration(\n        color: context.colors.surfaceContainerHigh,\n        borderRadius: BorderRadius.circular(12),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildTimeInfo(context, Symbols.schedule_rounded, '發送時間', sendTime),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTimeInfo(\n    BuildContext context,\n    IconData icon,\n    String label,\n    DateTime time,\n  ) {\n    return Row(\n      children: [\n        Icon(icon, color: context.colors.secondary),\n        const SizedBox(width: 8),\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              label,\n              style: context.theme.textTheme.labelLarge?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n            Text(\n              DateFormat('yyyy/MM/dd HH:mm').format(time),\n              style: context.theme.textTheme.bodyLarge,\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildAffectedAreas() {\n    final grouped = groupBy(\n      data.area.map((e) => Global.location[e.toString()]!),\n      (e) => e.cityWithLevel,\n    );\n    final List<Widget> areas = [];\n\n    for (final MapEntry(key: city, value: locations) in grouped.entries) {\n      areas.add(\n        Padding(\n          padding: const EdgeInsets.symmetric(vertical: 8),\n          child: Column(\n            children: [\n              Row(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.only(top: 8),\n                    child: Text(\n                      city,\n                      style: const TextStyle(fontWeight: FontWeight.bold),\n                    ),\n                  ),\n                  const SizedBox(width: 20),\n                  Expanded(\n                    child: Wrap(\n                      spacing: 8,\n                      runSpacing: 8,\n                      children: locations.map((e) {\n                        return Chip(\n                          padding: const EdgeInsets.all(4),\n                          side: BorderSide(color: context.colors.outline),\n                          backgroundColor: context.colors.surfaceContainerHigh,\n                          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n                          label: Text(e.townWithLevel),\n                        );\n                      }).toList(),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      );\n    }\n\n    return DetailFieldTile(\n      label: '影響區域',\n      child: Column(children: areas),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/event_viewer/thunderstorm.dart",
    "content": "import 'dart:async';\nimport 'dart:ui' as ui;\n\nimport 'package:collection/collection.dart';\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/api/route.dart';\nimport 'package:dpip/app/map/_widgets/map_legend.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/list_icon.dart';\nimport 'package:dpip/utils/serialization.dart';\nimport 'package:dpip/widgets/chip/label_chip.dart';\nimport 'package:dpip/widgets/list/detail_field_tile.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:dpip/widgets/sheet/bottom_sheet_drag_handle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:timezone/timezone.dart';\n\nclass ThunderstormPage extends StatefulWidget {\n  final History item;\n\n  const ThunderstormPage({super.key, required this.item});\n\n  @override\n  _ThunderstormPageState createState() => _ThunderstormPageState();\n}\n\nclass _ThunderstormPageState extends State<ThunderstormPage> {\n  late MapLibreMapController _mapController;\n  List<String> radarList = [];\n  double? userLat;\n  double? userLon;\n  bool isUserLocationValid = false;\n  bool _showLegend = false;\n  Timer? _blinkTimer;\n  int _blink = 0;\n  bool isExpired = true;\n\n  @override\n  void dispose() {\n    _mapController.dispose();\n    _blinkTimer?.cancel();\n    super.dispose();\n  }\n\n  void _initMap(MapLibreMapController controller) {\n    _mapController = controller;\n  }\n\n  Future<void> _loadMap() async {\n    final outlineColor = context.colors.outline.toHexStringRGB();\n\n    final list = await ExpTech().getRadarList();\n\n    if (mounted) {\n      setState(() {\n        radarList = list;\n      });\n    }\n    final String newTileUrl = radarTile(radarList.last);\n\n    await _mapController.addSource(\n      'radarSource',\n      RasterSourceProperties(tiles: [newTileUrl], tileSize: 256),\n    );\n\n    if (!isExpired) {\n      await _mapController.addLayer(\n        'radarSource',\n        'radarLayer',\n        const RasterLayerProperties(),\n        belowLayerId: BaseMapLayerIds.userLocation,\n      );\n    }\n\n    await _mapController.addLayer(\n      'exptech',\n      'town-outline-default',\n      LineLayerProperties(lineColor: outlineColor, lineWidth: 1),\n      sourceLayer: 'town',\n      belowLayerId: BaseMapLayerIds.userLocation,\n    );\n\n    await _mapController.addLayer(\n      'exptech',\n      'town-outline-highlighted',\n      const LineLayerProperties(lineColor: '#9e10fd', lineWidth: 2),\n      sourceLayer: 'town',\n      filter: [\n        'in',\n        ['get', 'CODE'],\n        ['literal', widget.item.area],\n      ],\n      belowLayerId: BaseMapLayerIds.userLocation,\n    );\n\n    if (GlobalProviders.location.auto) {\n      await updateLocationFromGPS();\n    }\n\n    await _mapController.addSource(\n      'markers-geojson',\n      const GeojsonSourceProperties(\n        data: {'type': 'FeatureCollection', 'features': []},\n      ),\n    );\n\n    start();\n\n    _blinkTimer = Timer.periodic(const Duration(milliseconds: 500), (\n      timer,\n    ) async {\n      if (!mounted) return;\n      await _mapController.setLayerProperties(\n        'town-outline-highlighted',\n        LineLayerProperties(lineOpacity: (_blink < 6) ? 1 : 0),\n      );\n      _blink++;\n      if (_blink >= 8) _blink = 0;\n    });\n  }\n\n  Future<void> start() async {\n    final location = GlobalProviders.location.coordinates;\n\n    if (location != null && location.isValid) {\n      await _mapController.animateCamera(\n        CameraUpdate.newLatLngZoom(location, 7.4),\n      );\n    } else {\n      await _mapController.animateCamera(\n        CameraUpdate.newLatLngZoom(DpipMap.kTaiwanCenter, 6.4),\n      );\n    }\n  }\n\n  void _toggleLegend() {\n    setState(() {\n      _showLegend = !_showLegend;\n    });\n  }\n\n  Widget _buildLegend() {\n    return ColorLegend(\n      reverse: true,\n      unit: 'dBZ',\n      items: [\n        ColorLegendItem(color: const Color(0xff00ffff), value: 0),\n        ColorLegendItem(color: const Color(0xff00a3ff), value: 5),\n        ColorLegendItem(color: const Color(0xff005bff), value: 10),\n        ColorLegendItem(\n          color: const Color(0xff0000ff),\n          value: 15,\n          blendTail: false,\n        ),\n        ColorLegendItem(\n          color: const Color(0xff00ff00),\n          value: 16,\n          hidden: true,\n        ),\n        ColorLegendItem(color: const Color(0xff00d300), value: 20),\n        ColorLegendItem(color: const Color(0xff00a000), value: 25),\n        ColorLegendItem(color: const Color(0xffccea00), value: 30),\n        ColorLegendItem(color: const Color(0xffffd300), value: 35),\n        ColorLegendItem(color: const Color(0xffff8800), value: 40),\n        ColorLegendItem(color: const Color(0xffff1800), value: 45),\n        ColorLegendItem(color: const Color(0xffd30000), value: 50),\n        ColorLegendItem(color: const Color(0xffa00000), value: 55),\n        ColorLegendItem(color: const Color(0xffea00cc), value: 60),\n        ColorLegendItem(color: const Color(0xff9600ff), value: 65),\n      ],\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final TZDateTime radarDateTime = TZDateTime.fromMillisecondsSinceEpoch(\n      UTC,\n      radarList.isEmpty ? 0 : int.parse(radarList.last),\n    );\n    final TZDateTime radarTime = TZDateTime.from(\n      radarDateTime,\n      getLocation('Asia/Taipei'),\n    );\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.item.text.content['all']?.title ?? ''),\n        elevation: 0,\n      ),\n      body: Stack(\n        children: [\n          DpipMap(onMapCreated: _initMap, onStyleLoadedCallback: _loadMap),\n          Positioned(\n            right: 4,\n            top: 4,\n            child: Material(\n              color: context.colors.secondary,\n              elevation: 4.0,\n              shape: const CircleBorder(),\n              clipBehavior: Clip.antiAlias,\n              child: InkWell(\n                onTap: _toggleLegend,\n                child: Tooltip(\n                  message: '雷達合成回波',\n                  child: Container(\n                    width: 30,\n                    height: 30,\n                    alignment: Alignment.center,\n                    child: Icon(\n                      _showLegend ? Icons.close : Icons.info_outline,\n                      size: 20,\n                      color: context.colors.onSecondary,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          if (_showLegend)\n            Positioned(\n              right: 6,\n              top: 50, // Adjusted to be above the legend button\n              child: _buildLegend(),\n            ),\n          Positioned(\n            left: 4,\n            top: 4,\n            child: ClipRRect(\n              borderRadius: BorderRadius.circular(5),\n              child: BackdropFilter(\n                filter: ui.ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),\n                child: Container(\n                  padding: const EdgeInsets.all(4),\n                  decoration: BoxDecoration(\n                    color: context.colors.surface.withValues(alpha: 0.5),\n                  ),\n                  child: Text(\n                    DateFormat('yyyy/MM/dd HH:mm').format(radarTime),\n                    style: TextStyle(\n                      fontSize: 12,\n                      fontWeight: FontWeight.bold,\n                      color: context.colors.onSurface,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          Positioned(\n            left: 4,\n            top: 32,\n            child: ClipRRect(\n              borderRadius: BorderRadius.circular(5),\n              child: BackdropFilter(\n                filter: ui.ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),\n                child: Container(\n                  padding: const EdgeInsets.all(4),\n                  decoration: BoxDecoration(\n                    color: context.colors.surface.withValues(alpha: 0.5),\n                  ),\n                  child: Text(\n                    '雷達合成回波',\n                    style: TextStyle(\n                      fontSize: 10,\n                      fontWeight: FontWeight.bold,\n                      color: context.colors.onSurface,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          _buildDraggableSheet(context),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildDraggableSheet(BuildContext context) {\n    return DraggableScrollableSheet(\n      initialChildSize: 0.35,\n      minChildSize: 0.15,\n      snap: true,\n      snapSizes: const [0.15, 0.35, 1],\n      builder: (context, scrollController) {\n        return Container(\n          decoration: BoxDecoration(\n            color: context.colors.surfaceContainer,\n            borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),\n            boxShadow: kElevationToShadow[4],\n          ),\n          child: ListView(\n            controller: scrollController,\n            padding: EdgeInsets.fromLTRB(16, 0, 16, context.padding.bottom),\n            children: [\n              const BottomSheetDragHandle(),\n              _buildWarningHeader(),\n              const Divider(),\n              _buildWarningDetails(),\n              const SizedBox(height: 20),\n              _buildAffectedAreas(),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildWarningHeader() {\n    final String subtitle = widget.item.text.content['all']?.subtitle ?? '';\n    final int expireTimestamp = widget.item.time.expires['all']!;\n    final TZDateTime expireTimeUTC = parseDateTime(expireTimestamp);\n    isExpired = TZDateTime.now(UTC).isAfter(expireTimeUTC.toUtc());\n\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.all(8),\n            decoration: BoxDecoration(\n              color: context.colors.secondaryContainer,\n              borderRadius: BorderRadius.circular(12),\n            ),\n            child: Icon(getListIcon(widget.item.icon), size: 28),\n          ),\n          const SizedBox(width: 12),\n          Row(\n            children: [\n              Text(\n                subtitle,\n                style: context.theme.textTheme.titleLarge?.copyWith(\n                  fontWeight: FontWeight.bold,\n                ),\n              ),\n              const SizedBox(width: 8),\n              if (isExpired)\n                LabelChip(\n                  label: '已結束',\n                  backgroundColor: context.colors.surfaceContainer,\n                  foregroundColor: context.colors.onSurfaceVariant,\n                )\n              else\n                LabelChip(\n                  label: '生效中',\n                  backgroundColor: context.colors.secondaryContainer,\n                  foregroundColor: context.colors.onSecondaryContainer,\n                  outlineColor: context.colors.secondaryContainer,\n                ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildWarningDetails() {\n    final DateTime sendTime = widget.item.time.send;\n    final int expireTimestamp = widget.item.time.expires['all']!;\n    final TZDateTime expireTimeUTC = parseDateTime(expireTimestamp);\n    final String description = widget.item.text.description['all'] ?? '';\n    final bool isExpired = TZDateTime.now(UTC).isAfter(expireTimeUTC.toUtc());\n    final DateTime localExpireTime = expireTimeUTC;\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Container(\n          margin: const EdgeInsets.symmetric(vertical: 8),\n          padding: const EdgeInsets.symmetric(horizontal: 8),\n          child: Text(description, style: context.theme.textTheme.bodyLarge),\n        ),\n        _buildTimeBar(context, sendTime, localExpireTime, isExpired),\n      ],\n    );\n  }\n\n  Widget _buildTimeBar(\n    BuildContext context,\n    DateTime sendTime,\n    DateTime expireTime,\n    bool isExpired,\n  ) {\n    final Duration duration = expireTime.difference(sendTime);\n    final Duration elapsed = isExpired ? duration : DateTime.now().difference(sendTime);\n    final double progress = elapsed.inSeconds / duration.inSeconds;\n\n    return Container(\n      margin: const EdgeInsets.symmetric(vertical: 8),\n      padding: const EdgeInsets.all(16),\n      decoration: BoxDecoration(\n        color: context.colors.surfaceContainerHigh,\n        borderRadius: BorderRadius.circular(12),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildTimeInfo(context, Symbols.schedule_rounded, '發送時間', sendTime),\n          const SizedBox(height: 12),\n          _buildTimeInfo(context, Symbols.flag_rounded, '有效至', expireTime),\n          const SizedBox(height: 16),\n          Stack(\n            children: [\n              LinearProgressIndicator(\n                value: progress.clamp(0.0, 1.0),\n                borderRadius: BorderRadius.circular(8),\n                color: isExpired ? context.colors.outline : context.colors.primary,\n                backgroundColor: context.colors.outlineVariant,\n              ),\n            ],\n          ),\n          const SizedBox(height: 8),\n          Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              Text(\n                DateFormat('MM/dd HH:mm').format(sendTime),\n                style: context.theme.textTheme.labelMedium,\n              ),\n              Text(\n                DateFormat('MM/dd HH:mm').format(expireTime),\n                style: context.theme.textTheme.labelMedium,\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTimeInfo(\n    BuildContext context,\n    IconData icon,\n    String label,\n    DateTime time,\n  ) {\n    return Row(\n      children: [\n        Icon(icon, color: context.colors.secondary),\n        const SizedBox(width: 8),\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              label,\n              style: context.theme.textTheme.labelLarge?.copyWith(\n                color: context.colors.onSurfaceVariant,\n              ),\n            ),\n            Text(\n              DateFormat('yyyy/MM/dd HH:mm').format(time),\n              style: context.theme.textTheme.bodyLarge,\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildAffectedAreas() {\n    final grouped = groupBy(\n      widget.item.area.map((e) => Global.location[e.toString()]!),\n      (e) => e.cityWithLevel,\n    );\n    final List<Widget> areas = [];\n\n    for (final MapEntry(key: city, value: locations) in grouped.entries) {\n      areas.add(\n        Padding(\n          padding: const EdgeInsets.symmetric(vertical: 8),\n          child: Column(\n            children: [\n              Row(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.only(top: 8),\n                    child: Text(\n                      city,\n                      style: const TextStyle(fontWeight: FontWeight.bold),\n                    ),\n                  ),\n                  const SizedBox(width: 20),\n                  Expanded(\n                    child: Wrap(\n                      spacing: 8,\n                      runSpacing: 8,\n                      children: locations.map((e) {\n                        return Chip(\n                          padding: const EdgeInsets.all(4),\n                          side: BorderSide(color: context.colors.outline),\n                          backgroundColor: context.colors.surfaceContainerHigh,\n                          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n                          label: Text(e.townWithLevel),\n                        );\n                      }).toList(),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      );\n    }\n\n    return DetailFieldTile(\n      label: '影響區域',\n      child: Column(children: areas),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/image_viewer/image_viewer.dart",
    "content": "import 'dart:io';\n\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:dio/dio.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/toast.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:gal/gal.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:permission_handler/permission_handler.dart';\n\nclass ImageViewerRoute extends StatefulWidget {\n  final String heroTag;\n  final String imageUrl;\n  final String imageName;\n\n  const ImageViewerRoute({\n    super.key,\n    required this.heroTag,\n    required this.imageUrl,\n    required this.imageName,\n  });\n\n  @override\n  State<ImageViewerRoute> createState() => _ImageViewerRouteState();\n}\n\nclass ImageSaver {\n  static const _channel = MethodChannel('image_saver');\n\n  static Future<void> saveToPhotos(String path) async {\n    await _channel.invokeMethod('saveImage', {\n      'path': path,\n    });\n  }\n}\n\nclass _ImageViewerRouteState extends State<ImageViewerRoute> {\n  final TransformationController _controller = TransformationController();\n  bool isDownloading = false;\n  bool isLoaded = false;\n  bool isUiHidden = false;\n\n  Future<void> saveImageToDownloads() async {\n    try {\n      PermissionStatus status;\n\n      if (Platform.isAndroid) {\n        final androidInfo = await DeviceInfoPlugin().androidInfo;\n        if (androidInfo.version.sdkInt <= 28) {\n          status = await Permission.storage.request();\n        } else {\n          status = PermissionStatus.granted;\n        }\n      } else {\n        status = await Permission.photosAddOnly.request();\n      }\n\n      if (!mounted) return;\n\n      if (status.isDenied || status.isPermanentlyDenied) {\n        showDialog(\n          context: context,\n          builder: (context) {\n            return AlertDialog(\n              icon: const Icon(Symbols.error),\n              title: Text('無法取得權限'.i18n),\n              content: Text(\n                \"儲存圖片需要您允許 DPIP 使用相片和媒體權限才能正常運作。${status.isPermanentlyDenied ? '請您到應用程式設定中找到並允許「相片和媒體」權限後再試一次。'.i18n : \"\"}\",\n              ),\n              actionsAlignment: MainAxisAlignment.spaceBetween,\n              actions: [\n                TextButton(\n                  child: Text('取消'.i18n),\n                  onPressed: () {\n                    Navigator.pop(context);\n                  },\n                ),\n                FilledButton(\n                  child: Text(\n                    status.isPermanentlyDenied ? '設定'.i18n : '再試一次'.i18n,\n                  ),\n                  onPressed: () {\n                    if (status.isPermanentlyDenied) {\n                      openAppSettings();\n                    } else {\n                      saveImageToDownloads();\n                    }\n\n                    Navigator.pop(context);\n                  },\n                ),\n              ],\n            );\n          },\n        );\n        return;\n      }\n\n      final res = await Dio().get<List<int>>(\n        widget.imageUrl,\n        options: Options(responseType: .bytes),\n      );\n\n      final tempDir = await getTemporaryDirectory();\n      final tempFile = File('${tempDir.path}/${widget.imageName}');\n      await tempFile.writeAsBytes(res.data!);\n\n      try {\n        if (Platform.isAndroid) {\n          await Gal.putImage(tempFile.path, album: 'DPIP');\n        } else {\n          await ImageSaver.saveToPhotos(tempFile.path);\n        }\n        if (!mounted) return;\n        showToast(\n          context,\n          ToastWidget.text(\n            '已儲存圖片'.i18n,\n            icon: const Icon(Symbols.check_rounded),\n          ),\n        );\n      } finally {\n        if (await tempFile.exists()) {\n          await tempFile.delete();\n        }\n      }\n    } catch (e) {\n      if (!mounted) return;\n\n      if (Platform.isIOS) {\n        showDialog(\n          context: context,\n          builder: (context) {\n            return AlertDialog(\n              icon: const Icon(Symbols.error),\n              title: Text('儲存圖片時發生錯誤'.i18n),\n              content: Text(e.toString()),\n              actions: [\n                TextButton(\n                  child: Text('確定'.i18n),\n                  onPressed: () {\n                    Navigator.pop(context);\n                  },\n                ),\n              ],\n            );\n          },\n        );\n      } else {\n        context.scaffoldMessenger.showSnackBar(\n          SnackBar(content: Text('儲存圖片時發生錯誤: $e'.i18n)),\n        );\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      extendBodyBehindAppBar: true,\n      body: Stack(\n        children: [\n          Center(\n            child: SizedBox.expand(\n              child: InteractiveViewer(\n                maxScale: 10,\n                transformationController: _controller,\n                onInteractionUpdate: (details) {\n                  if (_controller.value.getMaxScaleOnAxis() == 1.0) {\n                    if (isUiHidden) {\n                      setState(() => isUiHidden = false);\n                    }\n                  } else {\n                    if (!isUiHidden) {\n                      setState(() => isUiHidden = true);\n                    }\n                  }\n                },\n                onInteractionEnd: (details) {\n                  if (details.pointerCount == 0 && Velocity.zero == details.velocity) {\n                    if (isUiHidden) {\n                      setState(() => isUiHidden = false);\n                    } else {\n                      setState(() => isUiHidden = true);\n                    }\n                  }\n                },\n                child: Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: Hero(\n                    tag: widget.heroTag,\n                    child: CachedNetworkImage(\n                      imageUrl: widget.imageUrl,\n                      progressIndicatorBuilder: (context, url, progress) {\n                        return Center(\n                          child: CircularProgressIndicator(\n                            value: progress.progress,\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          AnimatedOpacity(\n            opacity: isUiHidden ? 0 : 1,\n            duration: const Duration(milliseconds: 200),\n            child: SafeArea(\n              child: Stack(\n                children: [\n                  Positioned(\n                    top: 8,\n                    left: 8,\n                    child: IconButton.filled(\n                      icon: const Icon(Symbols.close_rounded),\n                      style: ButtonStyle(\n                        foregroundColor: WidgetStateProperty.all(\n                          context.colors.onSurfaceVariant,\n                        ),\n                        backgroundColor: WidgetStateProperty.all(\n                          context.colors.surfaceContainerHighest.withValues(\n                            alpha: 0.8,\n                          ),\n                        ),\n                      ),\n                      onPressed: () {\n                        Navigator.maybePop(context);\n                      },\n                    ),\n                  ),\n                  Positioned(\n                    bottom: 16,\n                    right: 16,\n                    child: TextButton.icon(\n                      icon: isDownloading\n                          ? const SizedBox(\n                              height: 24,\n                              width: 24,\n                              child: Padding(\n                                padding: EdgeInsets.all(4.0),\n                                child: CircularProgressIndicator(\n                                  strokeWidth: 2,\n                                ),\n                              ),\n                            )\n                          : const Icon(Symbols.save_rounded),\n                      label: Text('儲存'.i18n),\n                      style: ButtonStyle(\n                        foregroundColor: WidgetStatePropertyAll(\n                          context.colors.onSurfaceVariant,\n                        ),\n                        backgroundColor: WidgetStatePropertyAll(\n                          context.colors.surfaceContainerHighest,\n                        ),\n                      ),\n                      onPressed: isDownloading\n                          ? null\n                          : () async {\n                              setState(() => isDownloading = true);\n                              await saveImageToDownloads();\n                              setState(() => isDownloading = false);\n                            },\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/log/log.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/material.dart';\nimport 'package:talker_flutter/talker_flutter.dart';\n\nclass LogViewerPage extends StatelessWidget {\n  const LogViewerPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: TalkerScreen(\n        talker: TalkerManager.instance,\n        appBarTitle: 'App 日誌',\n        theme: TalkerScreenTheme(\n          backgroundColor: context.colors.surface,\n          textColor: context.colors.onSurface,\n          cardColor: context.colors.surfaceContainer,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/notification/notification.dart",
    "content": "import 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/notification_record.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass NotificationHistoryPage extends StatefulWidget {\n  const NotificationHistoryPage({super.key});\n\n  @override\n  _NotificationHistoryPageState createState() => _NotificationHistoryPageState();\n}\n\nclass _NotificationHistoryPageState extends State<NotificationHistoryPage> {\n  List<NotificationRecord> notificationRecords = [];\n  bool isLoading = true;\n  String? errorMessage;\n\n  @override\n  void initState() {\n    super.initState();\n    _fetchNotificationRecords();\n  }\n\n  Future<void> _fetchNotificationRecords() async {\n    try {\n      final records = await ExpTech().getNotificationHistory();\n      notificationRecords = records.reversed.toList();\n    } catch (e) {\n      errorMessage = '取得通知紀錄時發生錯誤 $e';\n    } finally {\n      isLoading = false;\n      setState(() {});\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('行動通知推播紀錄'), elevation: 0),\n      body: SafeArea(child: _buildNotificationList()),\n    );\n  }\n\n  Widget _buildNotificationList() {\n    if (isLoading) {\n      return const Center(child: CircularProgressIndicator());\n    }\n    if (errorMessage != null) {\n      return Center(child: Text(errorMessage!));\n    }\n    if (notificationRecords.isEmpty) {\n      return const Center(child: Text('沒有通知紀錄'));\n    }\n    return RefreshIndicator(\n      onRefresh: _fetchNotificationRecords,\n      child: ListView.builder(\n        itemCount: notificationRecords.length,\n        itemBuilder: (context, index) {\n          return NotificationCard(\n            record: notificationRecords[index],\n            onTap: () {\n              // 導航到詳細資訊頁面\n              Navigator.push(\n                context,\n                MaterialPageRoute(\n                  builder: (context) => NotificationDetailPage(\n                    record: notificationRecords[index],\n                  ),\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass NotificationCard extends StatelessWidget {\n  final NotificationRecord record;\n  final VoidCallback onTap;\n\n  const NotificationCard({\n    super.key,\n    required this.record,\n    required this.onTap,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 4,\n      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),\n      child: InkWell(\n        onTap: onTap,\n        borderRadius: BorderRadius.circular(16),\n        child: Padding(\n          padding: const EdgeInsets.all(16),\n          child: Row(\n            children: [\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                      children: [\n                        Expanded(\n                          child: Text(\n                            record.title,\n                            style: context.theme.textTheme.titleMedium?.copyWith(\n                              fontWeight: FontWeight.bold,\n                            ),\n                            maxLines: 1,\n                            overflow: TextOverflow.ellipsis,\n                          ),\n                        ),\n                        _buildCriticalityChip(context),\n                      ],\n                    ),\n                    _buildDateChip(context),\n                    const SizedBox(height: 10),\n                    Text(\n                      record.body,\n                      style: context.theme.textTheme.bodyMedium,\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const SizedBox(height: 10),\n                    _buildAreaChips(context),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 10),\n              Icon(\n                Icons.arrow_forward_ios,\n                size: 16,\n                color: context.theme.textTheme.bodySmall?.color?.withValues(\n                  alpha: 0.5,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCriticalityChip(BuildContext context) {\n    return Chip(\n      label: Text(\n        record.critical ? '緊急' : '一般',\n        style: const TextStyle(fontWeight: FontWeight.bold),\n      ),\n      backgroundColor: record.critical\n          ? Colors.red.withValues(alpha: 0.16)\n          : Colors.grey.withValues(alpha: 0.16),\n      side: BorderSide(color: record.critical ? Colors.red : Colors.grey),\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n    );\n  }\n\n  Widget _buildDateChip(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Icon(Icons.access_time, size: 16, color: context.colors.primary),\n        const SizedBox(width: 4),\n        Text(\n          _formatDate(record.time),\n          style: TextStyle(\n            color: context.colors.primary,\n            fontWeight: FontWeight.bold,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildAreaChips(BuildContext context) {\n    return Wrap(\n      spacing: 4,\n      runSpacing: 4,\n      children:\n          record.area\n              .take(2)\n              .map(\n                (area) => Container(\n                  padding: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 4,\n                  ),\n                  decoration: BoxDecoration(\n                    color: Colors.blue.withValues(alpha: 0.16),\n                    borderRadius: BorderRadius.circular(5),\n                    border: Border.all(color: Colors.blue),\n                  ),\n                  child: Text(area, style: const TextStyle(fontSize: 12)),\n                ),\n              )\n              .toList() +\n          (record.area.length > 2\n              ? [\n                  Container(\n                    padding: const EdgeInsets.symmetric(\n                      horizontal: 8,\n                      vertical: 4,\n                    ),\n                    decoration: BoxDecoration(\n                      color: Colors.blue.withValues(alpha: 0.16),\n                      borderRadius: BorderRadius.circular(5),\n                      border: Border.all(color: Colors.blue),\n                    ),\n                    child: Text(\n                      '+${record.area.length - 2}',\n                      style: const TextStyle(fontSize: 12),\n                    ),\n                  ),\n                ]\n              : []),\n    );\n  }\n\n  String _formatDate(int timestamp) {\n    final date = DateTime.fromMillisecondsSinceEpoch(timestamp);\n    return DateFormat('yyyy/MM/dd HH:mm').format(date);\n  }\n}\n\nclass NotificationDetailPage extends StatelessWidget {\n  final NotificationRecord record;\n\n  const NotificationDetailPage({super.key, required this.record});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('通知詳細資訊'), elevation: 0),\n      body: SingleChildScrollView(\n        padding: const EdgeInsets.all(16.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              record.title,\n              style: context.theme.textTheme.headlineSmall?.copyWith(\n                fontWeight: FontWeight.bold,\n              ),\n            ),\n            const SizedBox(height: 16),\n            _buildDateChip(context),\n            const SizedBox(height: 16),\n            _buildCriticalityChip(context),\n            const SizedBox(height: 16),\n            Text(record.body, style: context.theme.textTheme.titleMedium),\n            const SizedBox(height: 24),\n            Text('通知發送區域', style: context.theme.textTheme.bodyMedium),\n            const SizedBox(height: 8),\n            _buildAreasList(context),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDateChip(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n      decoration: BoxDecoration(\n        color: context.colors.primary.withValues(alpha: 0.1),\n        borderRadius: BorderRadius.circular(20),\n        border: Border.all(\n          color: context.colors.primary.withValues(alpha: 0.3),\n        ),\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Icon(Icons.calendar_today, size: 18, color: context.colors.primary),\n          const SizedBox(width: 8),\n          Text(\n            _formatDate(record.time),\n            style: TextStyle(\n              color: context.colors.primary,\n              fontWeight: FontWeight.bold,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildCriticalityChip(BuildContext context) {\n    return Chip(\n      label: Text(\n        record.critical ? '緊急通知' : '一般通知',\n        style: const TextStyle(fontWeight: FontWeight.bold),\n      ),\n      backgroundColor: record.critical\n          ? Colors.red.withValues(alpha: 0.16)\n          : Colors.grey.withValues(alpha: 0.16),\n      side: BorderSide(color: record.critical ? Colors.red : Colors.grey),\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n    );\n  }\n\n  Widget _buildAreasList(BuildContext context) {\n    return Wrap(\n      spacing: 4,\n      runSpacing: 4,\n      children: record.area\n          .map(\n            (area) => Container(\n              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n              decoration: BoxDecoration(\n                color: Colors.blue.withValues(alpha: 0.16),\n                borderRadius: BorderRadius.circular(5),\n                border: Border.all(color: Colors.blue),\n              ),\n              child: Text(area, style: const TextStyle(fontSize: 12)),\n            ),\n          )\n          .toList(),\n    );\n  }\n\n  String _formatDate(int timestamp) {\n    final date = DateTime.fromMillisecondsSinceEpoch(timestamp);\n    return DateFormat('yyyy/MM/dd HH:mm:ss').format(date);\n  }\n}\n"
  },
  {
    "path": "lib/route/report/report.dart",
    "content": "import 'dart:async';\n\nimport 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/report/earthquake_report.dart';\nimport 'package:dpip/core/eew.dart';\nimport 'package:dpip/route/report/report_sheet_content.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:dpip/utils/extensions/iterable.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/utils/map_utils.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass ReportRoute extends StatefulWidget {\n  final String id;\n\n  const ReportRoute({super.key, required this.id});\n\n  @override\n  State<ReportRoute> createState() => _ReportRouteState();\n}\n\nclass _ReportRouteState extends State<ReportRoute> with TickerProviderStateMixin {\n  EarthquakeReport? report;\n  final mapController = Completer<MapLibreMapController>();\n\n  late final backgroundColor = Color.lerp(\n    context.colors.surface,\n    context.colors.surfaceTint,\n    0.08,\n  );\n\n  late final decorationTween = DecorationTween(\n    begin: BoxDecoration(\n      borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),\n      boxShadow: kElevationToShadow[4],\n      color: backgroundColor,\n    ),\n    end: BoxDecoration(\n      borderRadius: BorderRadius.zero,\n      boxShadow: kElevationToShadow[4],\n      color: backgroundColor,\n    ),\n  ).chain(CurveTween(curve: Curves.linear));\n\n  final opacityTween = Tween(\n    begin: 0.0,\n    end: 1.0,\n  ).chain(CurveTween(curve: Curves.linear));\n\n  late final sheetInitialSize = context.padding.bottom / context.dimension.height + 0.2;\n  final sheetController = DraggableScrollableController();\n  late final animController = AnimationController(\n    vsync: this,\n    duration: const Duration(milliseconds: 300),\n  );\n  late ScrollController scrollController;\n\n  bool isAppBarVisible = false;\n  bool isLoading = true;\n  bool isLoaded = false;\n\n  Future<void> refreshReport() async {\n    if (isLoaded) {\n      return;\n    }\n\n    setState(() => isLoading = true);\n\n    try {\n      final data = await ExpTech().getReport(widget.id);\n      final controller = await mapController.future;\n\n      final List markers = [];\n      final List<double> bounds = [];\n\n      final Map<String, int> cityMaxIntensity = {};\n\n      for (final MapEntry(key: areaName, value: area) in data.list.entries) {\n        for (final MapEntry(key: _, value: town) in area.town.entries) {\n          if (cityMaxIntensity[areaName] == null || cityMaxIntensity[areaName]! < town.intensity) {\n            cityMaxIntensity[areaName] = town.intensity;\n          }\n\n          markers.add({\n            'type': 'Feature',\n            'properties': {'intensity': town.intensity},\n            'geometry': {\n              'coordinates': [town.lon, town.lat],\n              'type': 'Point',\n            },\n          });\n\n          if (bounds.isEmpty) {\n            bounds.addAll([town.lat, town.lon, town.lat, town.lon]);\n          }\n\n          bounds.expandBounds(LatLng(town.lat, town.lon));\n        }\n      }\n\n      markers.add({\n        'type': 'Feature',\n        'properties': {\n          'intensity': 10, // 10 is for classifying epicenter cross\n        },\n        'geometry': {\n          'coordinates': data.latlng.asGeoJsonCooridnate,\n          'type': 'Point',\n        },\n      });\n\n      bounds.expandBounds(data.latlng);\n\n      await controller.moveCamera(\n        CameraUpdate.newLatLngBounds(\n          LatLngBounds(\n            southwest: LatLng(bounds[0], bounds[1]),\n            northeast: LatLng(bounds[2], bounds[3]),\n          ),\n          left: 32,\n          right: 32,\n          top: 32,\n          bottom: 212,\n        ),\n      );\n\n      if (controller.cameraPosition!.zoom > 9) {\n        await controller.moveCamera(CameraUpdate.zoomTo(9));\n      }\n\n      await controller.addGeoJsonSource('markers-geojson', {\n        'type': 'FeatureCollection',\n        'features': markers,\n      });\n\n      final waves = GeoJsonBuilder();\n\n      for (var i = 0; i < 10; i++) {\n        final distance = calcWaveRadius(\n          data.depth,\n          data.time.millisecondsSinceEpoch,\n          data.time.millisecondsSinceEpoch + i * 5000,\n        );\n\n        if (distance.s < 0) continue;\n\n        waves.addFeature(\n          circleFeature(\n            center: LatLng(data.latitude, data.longitude),\n            radius: distance.s,\n          ),\n        );\n      }\n\n      await controller.addGeoJsonSource('waves-geojson', waves.build());\n\n      if (!mounted) return;\n\n      // await controller.addLayer(\n      //   \"waves-geojson\",\n      //   \"waves\",\n      //   LineLayerProperties(\n      //     lineColor: context.colors.outline.toHexStringRGB(),\n      //   ),\n      // );\n\n      await controller.addLayer(\n        'markers-geojson',\n        'markers',\n        const SymbolLayerProperties(\n          symbolSortKey: [Expressions.get, 'intensity'],\n          symbolZOrder: 'source',\n          iconSize: [\n            Expressions.interpolate,\n            ['linear'],\n            [Expressions.zoom],\n            5,\n            0.5,\n            10,\n            1.5,\n          ],\n          iconImage: [\n            Expressions.match,\n            [Expressions.get, 'intensity'],\n            1,\n            'intensity-1',\n            2,\n            'intensity-2',\n            3,\n            'intensity-3',\n            4,\n            'intensity-4',\n            5,\n            'intensity-5',\n            6,\n            'intensity-6',\n            7,\n            'intensity-7',\n            8,\n            'intensity-8',\n            9,\n            'intensity-9',\n            'cross',\n          ],\n          iconAllowOverlap: true,\n          iconIgnorePlacement: true,\n        ),\n      );\n\n      if (!mounted) return;\n\n      await controller.setLayerProperties(\n        'county',\n        FillLayerProperties(\n          fillColor: [\n            'match',\n            ['get', 'NAME'],\n            ...cityMaxIntensity.entries.expand(\n              (entry) => [\n                entry.key,\n                IntensityColor.intensity(entry.value).toHexStringRGB(),\n              ],\n            ),\n            context.colors.surfaceContainerHighest.toHexStringRGB(),\n          ],\n          fillOpacity: 1,\n        ),\n      );\n\n      await controller.setLayerProperties(\n        'town',\n        const FillLayerProperties(fillOpacity: 0),\n      );\n\n      setState(() {\n        report = data;\n        isLoading = false;\n        isLoaded = true;\n      });\n    } catch (e) {\n      TalkerManager.instance.error(e);\n      setState(() => isLoading = false);\n    }\n  }\n\n  Future<void> focus(LatLng target) async {\n    final controller = await mapController.future;\n    sheetController.animateTo(\n      sheetInitialSize,\n      duration: Durations.short4,\n      curve: Easing.standard,\n    );\n    scrollController.jumpTo(0);\n    controller.animateCamera(\n      CameraUpdate.newLatLngZoom(\n        LatLng(target.latitude - 0.03, target.longitude),\n        10,\n      ),\n      duration: const Duration(seconds: 1),\n    );\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    sheetController.addListener(() {\n      final newSize = sheetController.size;\n      final double scrollPosition = ((newSize - sheetInitialSize) / (1 - sheetInitialSize)).clamp(\n        0.0,\n        1.0,\n      );\n\n      if (scrollPosition > 1e-5) {\n        if (!isAppBarVisible) {\n          setState(() => isAppBarVisible = true);\n        }\n      } else {\n        if (isAppBarVisible) {\n          setState(() => isAppBarVisible = false);\n        }\n      }\n      animController.animateTo(scrollPosition, duration: Duration.zero);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final appBar = AppBar(elevation: 4, title: const Text('地震報告'));\n\n    return Scaffold(\n      body: Stack(\n        children: [\n          DpipMap(\n            onMapCreated: (controller) async {\n              mapController.complete(controller);\n              await controller.setSymbolIconAllowOverlap(true);\n              await controller.setSymbolIconIgnorePlacement(true);\n              refreshReport();\n            },\n          ),\n          if (report != null)\n            Positioned(\n              top: context.padding.top + 50,\n              left: 0,\n              right: 0,\n              child: Column(\n                children: [\n                  // 不要翻譯這\n                  if (report!.magnitude >= 6 &&\n                      report!.magnitude < 7 &&\n                      report!.depth <= 35 &&\n                      (report!.getLocation().endsWith('近海') ||\n                          report!.getLocation().endsWith('海域')))\n                    Chip(\n                      avatar: Icon(\n                        Symbols.tsunami_rounded,\n                        color: context.theme.extendedColors.blue,\n                      ),\n                      label: Text(\n                        '此地震可能引起若干海面變動',\n                        style: TextStyle(\n                          color: context.theme.extendedColors.blue,\n                        ),\n                      ),\n                      backgroundColor: Colors.blue.withValues(alpha: 0.16),\n                      labelStyle: const TextStyle(fontWeight: FontWeight.w900),\n                      side: BorderSide(\n                        color: context.theme.extendedColors.blue,\n                      ),\n                    ),\n                  // 不要翻譯這\n                  if (report!.magnitude >= 7 &&\n                      report!.depth <= 35 &&\n                      (report!.getLocation().endsWith('近海') ||\n                          report!.getLocation().endsWith('海域')))\n                    Chip(\n                      avatar: Icon(\n                        Symbols.tsunami_rounded,\n                        color: context.colors.error,\n                      ),\n                      label: Text(\n                        '此地震可能引起海嘯 注意後續資訊',\n                        style: TextStyle(color: context.colors.error),\n                      ),\n                      backgroundColor: Colors.red.withValues(alpha: 0.16),\n                      labelStyle: const TextStyle(fontWeight: FontWeight.w900),\n                      side: BorderSide(color: context.colors.error),\n                    ),\n                ],\n              ),\n            ),\n          Positioned(\n            top: context.padding.top + 4,\n            left: 4,\n            child: BackButton(\n              style: ButtonStyle(\n                elevation: const WidgetStatePropertyAll(4),\n                shadowColor: WidgetStatePropertyAll(context.colors.shadow),\n                surfaceTintColor: WidgetStatePropertyAll(\n                  context.colors.surfaceTint,\n                ),\n                backgroundColor: WidgetStatePropertyAll(context.colors.surface),\n              ),\n            ),\n          ),\n          Positioned.fill(\n            top: context.padding.top + appBar.preferredSize.height - 24,\n            child: DraggableScrollableSheet(\n              key: const GlobalObjectKey('DraggableScrollableSheet'),\n              initialChildSize: sheetInitialSize,\n              minChildSize: sheetInitialSize,\n              controller: sheetController,\n              snap: true,\n              builder: (context, controller) {\n                scrollController = controller;\n\n                return DecoratedBoxTransition(\n                  decoration: animController.drive(decorationTween),\n                  child: isLoading\n                      ? const Center(child: CircularProgressIndicator())\n                      : report == null\n                      ? Padding(\n                          padding: const EdgeInsets.all(20),\n                          child: Row(\n                            children: [\n                              const Flexible(\n                                flex: 8,\n                                child: Text(\n                                  '取得地震報告時發生錯誤，請檢查網路狀況後再試一次。',\n                                  style: TextStyle(fontSize: 16),\n                                ),\n                              ),\n                              const SizedBox(width: 10),\n                              Flexible(\n                                flex: 2,\n                                child: IconButton(\n                                  icon: const Icon(Symbols.refresh),\n                                  style: ElevatedButton.styleFrom(\n                                    foregroundColor: context.colors.onSurface,\n                                  ),\n                                  onPressed: () {\n                                    refreshReport();\n                                  },\n                                ),\n                              ),\n                            ],\n                          ),\n                        )\n                      : ReportSheetContent(\n                          report: report!,\n                          controller: controller,\n                          focus: focus,\n                        ),\n                );\n              },\n            ),\n          ),\n          Positioned(\n            top: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: isAppBarVisible,\n              child: FadeTransition(\n                opacity: animController.drive(opacityTween),\n                child: appBar,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/report/report_sheet_content.dart",
    "content": "import 'package:dpip/api/model/report/earthquake_report.dart';\nimport 'package:dpip/app/map/page.dart';\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/depth_color.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/utils/magnitude_color.dart';\nimport 'package:dpip/widgets/list/detail_field_tile.dart';\nimport 'package:dpip/widgets/report/enlargeable_image.dart';\nimport 'package:dpip/widgets/report/intensity_box.dart';\nimport 'package:dpip/widgets/sheet/bottom_sheet_drag_handle.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:material_symbols_icons/symbols.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass ReportSheetContent extends StatelessWidget {\n  final ScrollController controller;\n  final EarthquakeReport report;\n  final void Function(LatLng target) focus;\n\n  const ReportSheetContent({\n    super.key,\n    required this.report,\n    required this.controller,\n    required this.focus,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: EdgeInsets.only(\n        bottom: context.padding.bottom,\n      ).copyWith(left: 16, right: 16),\n      controller: controller,\n      children: [\n        const BottomSheetDragHandle(),\n        Padding(\n          padding: const EdgeInsets.symmetric(vertical: 8),\n          child: Row(\n            children: [\n              IntensityBox(intensity: report.getMaxIntensity()),\n              const SizedBox(width: 16),\n              Expanded(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      report.hasNumber ? '編號 ${report.number} 顯著有感地震' : '小區域有感地震',\n                      style: TextStyle(\n                        color: context.colors.onSurfaceVariant,\n                        fontSize: 14,\n                      ),\n                    ),\n                    Text(\n                      report.getLocation(),\n                      style: const TextStyle(\n                        fontSize: 20,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n        Wrap(\n          spacing: 8,\n          children: [\n            ActionChip(\n              avatar: Icon(\n                Symbols.open_in_new,\n                color: context.colors.onPrimary,\n              ),\n              label: const Text('報告頁面'),\n              backgroundColor: context.colors.primary,\n              labelStyle: TextStyle(color: context.colors.onPrimary),\n              side: BorderSide(color: context.colors.primary),\n              onPressed: () {\n                launchUrl(report.reportUrl);\n              },\n            ),\n            ActionChip(\n              avatar: const Icon(Symbols.replay),\n              label: Text('重播'.i18n),\n              onPressed: () {\n                Navigator.push(\n                  context,\n                  MaterialPageRoute(\n                    builder: (context) => MapMonitorPage(\n                      replayTimestamp: report.time.millisecondsSinceEpoch - 2000,\n                    ),\n                  ),\n                );\n              },\n            ),\n          ],\n        ),\n        const Divider(),\n        DetailFieldTile(\n          label: '發震時間',\n          child: Text(\n            DateFormat('yyyy/MM/dd HH:mm:ss').format(report.time),\n            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\n          ),\n        ),\n        DetailFieldTile(\n          label: '位於',\n          child: Text(\n            report.convertLatLon(),\n            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\n          ),\n        ),\n        Row(\n          children: [\n            Expanded(\n              child: DetailFieldTile(\n                label: '地震規模',\n                child: Row(\n                  children: [\n                    Container(\n                      height: 12,\n                      width: 12,\n                      margin: const EdgeInsets.only(right: 6),\n                      decoration: BoxDecoration(\n                        borderRadius: BorderRadius.circular(10),\n                        color: MagnitudeColor.magnitude(report.magnitude),\n                      ),\n                    ),\n                    Text(\n                      'M ${report.magnitude}',\n                      style: const TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            Expanded(\n              child: DetailFieldTile(\n                label: '震源深度',\n                child: Row(\n                  children: [\n                    Container(\n                      height: 12,\n                      width: 12,\n                      margin: const EdgeInsets.only(right: 6),\n                      decoration: BoxDecoration(\n                        borderRadius: BorderRadius.circular(10),\n                        color: getDepthColor(report.depth),\n                      ),\n                    ),\n                    Text(\n                      '${report.depth} km',\n                      style: const TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        ),\n        const Divider(),\n        DetailFieldTile(\n          label: '各地震度',\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              for (final MapEntry(key: areaName, value: area) in report.list.entries)\n                Padding(\n                  padding: const EdgeInsets.symmetric(vertical: 8),\n                  child: Column(\n                    children: [\n                      Row(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Padding(\n                            padding: const EdgeInsets.only(top: 8),\n                            child: Text(\n                              areaName,\n                              style: const TextStyle(\n                                fontWeight: FontWeight.bold,\n                              ),\n                            ),\n                          ),\n                          const SizedBox(width: 20),\n                          Expanded(\n                            child: Wrap(\n                              spacing: 8,\n                              runSpacing: 8,\n                              children: [\n                                for (final MapEntry(key: townName, value: town)\n                                    in area.town.entries)\n                                  ActionChip(\n                                    padding: const EdgeInsets.all(4),\n                                    side: BorderSide(\n                                      color: IntensityColor.intensity(\n                                        town.intensity,\n                                      ),\n                                    ),\n                                    backgroundColor: IntensityColor.intensity(\n                                      town.intensity,\n                                    ).withValues(alpha: 0.16),\n                                    materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n                                    avatar: AspectRatio(\n                                      aspectRatio: 1,\n                                      child: Container(\n                                        decoration: BoxDecoration(\n                                          borderRadius: BorderRadius.circular(\n                                            6,\n                                          ),\n                                          color: IntensityColor.intensity(\n                                            town.intensity,\n                                          ),\n                                        ),\n                                        child: Center(\n                                          child: Text(\n                                            town.intensity.asIntensityDisplayLabel,\n                                            style: TextStyle(\n                                              height: 1,\n                                              fontSize: 15,\n                                              fontWeight: FontWeight.bold,\n                                              color: IntensityColor.onIntensity(\n                                                town.intensity,\n                                              ),\n                                            ),\n                                          ),\n                                        ),\n                                      ),\n                                    ),\n                                    label: Text(townName),\n                                    onPressed: () => focus(LatLng(town.lat, town.lon)),\n                                  ),\n                              ],\n                            ),\n                          ),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n            ],\n          ),\n        ),\n        const Divider(),\n        DetailFieldTile(\n          label: '地震報告圖',\n          child: EnlargeableImage(\n            aspectRatio: 4 / 3,\n            heroTag: 'report-image-${report.id}',\n            imageUrl: report.reportImageUrl,\n            imageName: report.reportImageName,\n          ),\n        ),\n        if (report.hasNumber)\n          DetailFieldTile(\n            label: '震度圖',\n            child: EnlargeableImage(\n              aspectRatio: 2334 / 2977,\n              heroTag: 'intensity-image-${report.id}',\n              imageUrl: report.intensityMapImageUrl!,\n              imageName: report.intensityMapImageName!,\n            ),\n          ),\n        if (report.hasNumber)\n          DetailFieldTile(\n            label: '最大地動加速度圖',\n            child: EnlargeableImage(\n              aspectRatio: 2334 / 2977,\n              heroTag: 'pga-image-${report.id}',\n              imageUrl: report.pgaMapImageUrl!,\n              imageName: report.pgaMapImageName!,\n            ),\n          ),\n        if (report.hasNumber)\n          DetailFieldTile(\n            label: '最大地動速度圖',\n            child: EnlargeableImage(\n              aspectRatio: 2334 / 2977,\n              heroTag: 'pgv-image-${report.id}',\n              imageUrl: report.pgvMapImageUrl!,\n              imageName: report.pgvMapImageName!,\n            ),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/route/status/status.dart",
    "content": "import 'package:dpip/api/exptech.dart';\nimport 'package:dpip/api/model/server_status.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass ServerStatusPage extends StatefulWidget {\n  const ServerStatusPage({super.key});\n\n  @override\n  _ServerStatusPageState createState() => _ServerStatusPageState();\n}\n\nclass _ServerStatusPageState extends State<ServerStatusPage> {\n  late Future<List<ServerStatus>> _statusFuture;\n  final Set<String> _expandedCards = <String>{};\n\n  @override\n  void initState() {\n    super.initState();\n    _statusFuture = _fetchServerStatus();\n  }\n\n  Future<List<ServerStatus>> _fetchServerStatus() async {\n    return (await ExpTech().getStatus()).reversed.toList();\n  }\n\n  void _toggleExpanded(String cardId) {\n    setState(() {\n      if (_expandedCards.contains(cardId)) {\n        _expandedCards.remove(cardId);\n      } else {\n        _expandedCards.add(cardId);\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isDarkMode = context.theme.brightness == Brightness.dark;\n\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text(\n          '伺服器狀態',\n          style: TextStyle(fontWeight: FontWeight.bold),\n        ),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.refresh),\n            onPressed: () {\n              setState(() {\n                _statusFuture = _fetchServerStatus();\n              });\n            },\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          _buildInfoBox(isDarkMode),\n          Expanded(\n            child: FutureBuilder<List<ServerStatus>>(\n              future: _statusFuture,\n              builder: (context, snapshot) {\n                if (snapshot.connectionState == ConnectionState.waiting) {\n                  return const Center(child: CircularProgressIndicator());\n                } else if (snapshot.hasError) {\n                  return Center(\n                    child: Text(\n                      '錯誤: ${snapshot.error}',\n                      style: TextStyle(\n                        color: context.colors.error,\n                        fontSize: 16,\n                      ),\n                    ),\n                  );\n                } else if (!snapshot.hasData || snapshot.data!.isEmpty) {\n                  return const Center(\n                    child: Text(\n                      '沒有可用的資料',\n                      style: TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.w500,\n                      ),\n                    ),\n                  );\n                }\n\n                final statuses = snapshot.data!;\n                return ListView.builder(\n                  itemCount: statuses.length,\n                  itemBuilder: (context, index) {\n                    return _buildStatusCard(statuses[index], isDarkMode);\n                  },\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildInfoBox(bool isDarkMode) {\n    return Container(\n      margin: const EdgeInsets.all(16),\n      padding: const EdgeInsets.all(16),\n      decoration: BoxDecoration(\n        color: isDarkMode ? Colors.grey[800] : Colors.blue[50],\n        borderRadius: BorderRadius.circular(12),\n        boxShadow: [\n          BoxShadow(\n            color: isDarkMode ? Colors.black26 : Colors.grey.withValues(alpha: 0.3),\n            spreadRadius: 1,\n            blurRadius: 5,\n            offset: const Offset(0, 3),\n          ),\n        ],\n      ),\n      child: Row(\n        children: [\n          Icon(\n            Icons.info_outline,\n            color: isDarkMode ? Colors.blue[300] : Colors.blue[700],\n          ),\n          const SizedBox(width: 16),\n          Expanded(\n            child: Text(\n              '此頁面呈現伺服器各時段狀態概覽。原始資料每5秒更新一次，此處顯示精簡版本以最佳化網路用量。請注意，此資訊僅供參考，實際狀況應以公告為準。',\n              style: TextStyle(\n                fontSize: 14,\n                color: isDarkMode ? Colors.white70 : Colors.black87,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildStatusCard(ServerStatus status, bool isDarkMode) {\n    final allServices = status.services.values.toList();\n    final normalServices = allServices.where((s) => s.status == 1).length;\n    final abnormalServices = allServices.length - normalServices;\n\n    Color statusColor;\n    IconData statusIcon;\n    String statusText;\n\n    if (abnormalServices == 0) {\n      statusColor = Colors.green;\n      statusIcon = Icons.check_circle;\n      statusText = '全部正常';\n    } else if (abnormalServices == allServices.length) {\n      statusColor = Colors.red;\n      statusIcon = Icons.error;\n      statusText = '全部異常';\n    } else {\n      statusColor = Colors.orange;\n      statusIcon = Icons.warning;\n      statusText = '部分異常';\n    }\n\n    final cardId = status.formattedTime;\n    final isExpanded = _expandedCards.contains(cardId);\n\n    return Card(\n      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n      elevation: 4,\n      color: isDarkMode ? Colors.grey[850] : Colors.white,\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n      child: Column(\n        children: [\n          InkWell(\n            onTap: () => _toggleExpanded(cardId),\n            child: Container(\n              padding: const EdgeInsets.all(16),\n              decoration: BoxDecoration(\n                color: isDarkMode\n                    ? statusColor.withValues(alpha: 0.2)\n                    : statusColor.withValues(alpha: 0.1),\n                borderRadius: const BorderRadius.all(Radius.circular(12)),\n              ),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                children: [\n                  Text(\n                    _formatDateTime(status.formattedTime),\n                    style: TextStyle(\n                      fontWeight: FontWeight.bold,\n                      fontSize: 18,\n                      color: isDarkMode ? Colors.white : Colors.black87,\n                    ),\n                  ),\n                  Row(\n                    children: [\n                      Container(\n                        padding: const EdgeInsets.symmetric(\n                          horizontal: 12,\n                          vertical: 6,\n                        ),\n                        decoration: BoxDecoration(\n                          color: isDarkMode\n                              ? statusColor.withValues(alpha: 0.3)\n                              : statusColor.withValues(alpha: 0.2),\n                          borderRadius: BorderRadius.circular(20),\n                        ),\n                        child: Row(\n                          mainAxisSize: MainAxisSize.min,\n                          children: [\n                            Icon(statusIcon, color: statusColor, size: 18),\n                            const SizedBox(width: 6),\n                            Text(\n                              statusText,\n                              style: TextStyle(\n                                color: statusColor,\n                                fontWeight: FontWeight.bold,\n                              ),\n                            ),\n                          ],\n                        ),\n                      ),\n                      const SizedBox(width: 8),\n                      Icon(\n                        isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,\n                        color: isDarkMode ? Colors.white70 : Colors.grey,\n                      ),\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ),\n          if (isExpanded)\n            ListView.separated(\n              shrinkWrap: true,\n              physics: const NeverScrollableScrollPhysics(),\n              itemCount: status.services.length,\n              separatorBuilder: (context, index) => Divider(\n                height: 1,\n                color: isDarkMode ? Colors.grey[700] : Colors.grey[300],\n              ),\n              itemBuilder: (context, index) {\n                final entry = status.services.entries.elementAt(index);\n                return _buildServiceTile(entry.key, entry.value, isDarkMode);\n              },\n            ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildServiceTile(\n    String serviceName,\n    ServiceStatus serviceStatus,\n    bool isDarkMode,\n  ) {\n    Color getStatusColor(int status) {\n      switch (status) {\n        case 1:\n          return Colors.green;\n        case 2:\n          return Colors.orange;\n        case -1:\n          return isDarkMode ? Colors.grey : Colors.grey[600]!;\n        default:\n          return Colors.red;\n      }\n    }\n\n    String getStatusText(int status) {\n      switch (status) {\n        case 1:\n          return '正常';\n        case 2:\n          return '不穩定';\n        case -1:\n          return '無資料';\n        default:\n          return '異常';\n      }\n    }\n\n    final statusColor = getStatusColor(serviceStatus.status);\n    final statusText = getStatusText(serviceStatus.status);\n\n    return ListTile(\n      title: Text(\n        serviceName,\n        style: TextStyle(\n          fontWeight: FontWeight.w500,\n          color: isDarkMode ? Colors.white : Colors.black87,\n        ),\n      ),\n      subtitle: Text(\n        '延遲: ${serviceStatus.count} ms',\n        style: TextStyle(color: isDarkMode ? Colors.white70 : Colors.black54),\n      ),\n      trailing: Container(\n        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n        decoration: BoxDecoration(\n          color: isDarkMode\n              ? statusColor.withValues(alpha: 0.3)\n              : statusColor.withValues(alpha: 0.1),\n          borderRadius: BorderRadius.circular(12),\n        ),\n        child: Text(\n          statusText,\n          style: TextStyle(color: statusColor, fontWeight: FontWeight.bold),\n        ),\n      ),\n    );\n  }\n\n  String _formatDateTime(String dateTimeString) {\n    final dateTime = DateTime.parse(dateTimeString);\n    final formatter = DateFormat('yyyy/MM/dd HH:mm');\n    return formatter.format(dateTime);\n  }\n}\n"
  },
  {
    "path": "lib/router.dart",
    "content": "import 'package:dpip/app/changelog/page.dart';\nimport 'package:dpip/app/debug/logs/page.dart';\nimport 'package:dpip/app/home/layout.dart';\nimport 'package:dpip/app/home/page.dart';\nimport 'package:dpip/app/map/page.dart';\nimport 'package:dpip/app/settings/donate/page.dart';\nimport 'package:dpip/app/settings/experimental/page.dart';\nimport 'package:dpip/app/settings/layout.dart';\nimport 'package:dpip/app/settings/layout/page.dart';\nimport 'package:dpip/app/settings/locale/page.dart';\nimport 'package:dpip/app/settings/locale/select/page.dart';\nimport 'package:dpip/app/settings/location/page.dart';\nimport 'package:dpip/app/settings/location/select/%5Bcity%5D/page.dart';\nimport 'package:dpip/app/settings/location/select/page.dart';\nimport 'package:dpip/app/settings/map/page.dart';\nimport 'package:dpip/app/settings/notify/(1.eew)/eew/page.dart';\nimport 'package:dpip/app/settings/notify/(2.earthquake)/intensity/page.dart';\nimport 'package:dpip/app/settings/notify/(2.earthquake)/monitor/page.dart';\nimport 'package:dpip/app/settings/notify/(2.earthquake)/report/page.dart';\nimport 'package:dpip/app/settings/notify/(3.weather)/advisory/page.dart';\nimport 'package:dpip/app/settings/notify/(3.weather)/evacuation/page.dart';\nimport 'package:dpip/app/settings/notify/(3.weather)/thunderstorm/page.dart';\nimport 'package:dpip/app/settings/notify/(4.tsunami)/tsunami/page.dart';\nimport 'package:dpip/app/settings/notify/(5.basic)/announcement/page.dart';\nimport 'package:dpip/app/settings/notify/page.dart';\nimport 'package:dpip/app/settings/page.dart';\nimport 'package:dpip/app/settings/proxy/page.dart';\nimport 'package:dpip/app/settings/theme/color/page.dart';\nimport 'package:dpip/app/settings/theme/mode/page.dart';\nimport 'package:dpip/app/settings/theme/page.dart';\nimport 'package:dpip/app/settings/unit/page.dart';\nimport 'package:dpip/app/welcome/1-about/page.dart';\nimport 'package:dpip/app/welcome/2-exptech/page.dart';\nimport 'package:dpip/app/welcome/3-notice/page.dart';\nimport 'package:dpip/app/welcome/4-permissions/page.dart';\nimport 'package:dpip/core/preference.dart';\nimport 'package:dpip/route/announcement/announcement.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/shell_wrapper.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:talker_flutter/talker_flutter.dart';\n\npart 'router.g.dart';\n\nfinal GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>();\nfinal GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey<NavigatorState>();\n\nfinal RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();\n\n/// Welcome route - displays the welcome/onboarding page.\n@TypedGoRoute<WelcomeRoute>(path: '/welcome')\nclass WelcomeRoute extends GoRouteData with $WelcomeRoute {\n  /// Creates a [WelcomeRoute].\n  const WelcomeRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const WelcomeAboutPage();\n  }\n}\n\n/// Welcome ExpTech route - displays ExpTech introduction page.\n@TypedGoRoute<WelcomeExptechRoute>(path: '/welcome/exptech')\nclass WelcomeExptechRoute extends GoRouteData with $WelcomeExptechRoute {\n  /// Creates a [WelcomeExptechRoute].\n  const WelcomeExptechRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const WelcomeExpTechPage();\n  }\n}\n\n/// Welcome Notice route - displays notice/disclaimer page.\n@TypedGoRoute<WelcomeNoticeRoute>(path: '/welcome/notice')\nclass WelcomeNoticeRoute extends GoRouteData with $WelcomeNoticeRoute {\n  /// Creates a [WelcomeNoticeRoute].\n  const WelcomeNoticeRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const WelcomeNoticePage();\n  }\n}\n\n/// Welcome Permissions route - displays permissions request page.\n@TypedGoRoute<WelcomePermissionsRoute>(path: '/welcome/permissions')\nclass WelcomePermissionsRoute extends GoRouteData with $WelcomePermissionsRoute {\n  /// Creates a [WelcomePermissionsRoute].\n  const WelcomePermissionsRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const WelcomePermissionPage();\n  }\n}\n\n/// Home route - displays the main application home page.\n@TypedGoRoute<HomeRoute>(path: '/home')\nclass HomeRoute extends GoRouteData with $HomeRoute {\n  /// Creates a [HomeRoute].\n  const HomeRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const HomeLayout(child: HomePage());\n  }\n}\n\n// Settings Shell Route and Children\n@TypedShellRoute<SettingsShellRoute>(\n  routes: <TypedGoRoute<GoRouteData>>[\n    TypedGoRoute<SettingsIndexRoute>(\n      path: '/settings',\n      routes: [\n        TypedGoRoute<SettingsLayoutRoute>(path: 'layout'),\n        TypedGoRoute<SettingsLocationRoute>(\n          path: 'location',\n          routes: [\n            TypedGoRoute<SettingsLocationSelectRoute>(\n              path: 'select',\n              routes: [\n                TypedGoRoute<SettingsLocationSelectCityRoute>(\n                  path: ':city',\n                ),\n              ],\n            ),\n          ],\n        ),\n        TypedGoRoute<SettingsThemeRoute>(\n          path: 'theme',\n          routes: [\n            TypedGoRoute<SettingsThemeModeRoute>(path: 'mode'),\n            TypedGoRoute<SettingsThemeColorRoute>(path: 'color'),\n          ],\n        ),\n        TypedGoRoute<SettingsLocaleRoute>(\n          path: 'locale',\n          routes: [\n            TypedGoRoute<SettingsLocaleSelectRoute>(path: 'select'),\n          ],\n        ),\n        TypedGoRoute<SettingsUnitRoute>(path: 'unit'),\n        TypedGoRoute<SettingsMapRoute>(path: 'map'),\n        TypedGoRoute<SettingsProxyRoute>(path: 'proxy'),\n        TypedGoRoute<SettingsExperimentalRoute>(path: 'experimental'),\n        TypedGoRoute<SettingsNotifyRoute>(\n          path: 'notify',\n          routes: <TypedGoRoute<GoRouteData>>[\n            TypedGoRoute<SettingsNotifyEewRoute>(path: 'eew'),\n            TypedGoRoute<SettingsNotifyMonitorRoute>(path: 'monitor'),\n            TypedGoRoute<SettingsNotifyReportRoute>(path: 'report'),\n            TypedGoRoute<SettingsNotifyIntensityRoute>(path: 'intensity'),\n            TypedGoRoute<SettingsNotifyThunderstormRoute>(path: 'thunderstorm'),\n            TypedGoRoute<SettingsNotifyAdvisoryRoute>(path: 'advisory'),\n            TypedGoRoute<SettingsNotifyEvacuationRoute>(path: 'evacuation'),\n            TypedGoRoute<SettingsNotifyTsunamiRoute>(path: 'tsunami'),\n            TypedGoRoute<SettingsNotifyAnnouncementRoute>(path: 'announcement'),\n          ],\n        ),\n        TypedGoRoute<SettingsDonateRoute>(path: 'donate'),\n      ],\n    ),\n  ],\n)\n/// Settings shell route - wraps all settings pages with a common layout.\nclass SettingsShellRoute extends ShellRouteData {\n  /// Creates a [SettingsShellRoute].\n  const SettingsShellRoute();\n\n  /// Navigator key for the settings shell route.\n  static final GlobalKey<NavigatorState> $navigatorKey = _settingsNavigatorKey;\n\n  @override\n  Widget builder(BuildContext context, GoRouterState state, Widget navigator) {\n    return ShellWrapper(\n      SettingsLayout(child: navigator),\n    );\n  }\n}\n\n/// Settings index route - displays the main settings page.\nclass SettingsIndexRoute extends GoRouteData with $SettingsIndexRoute {\n  /// Creates a [SettingsIndexRoute].\n  const SettingsIndexRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsIndexPage());\n  }\n}\n\n/// Settings location route - displays location settings.\nclass SettingsLocationRoute extends GoRouteData with $SettingsLocationRoute {\n  /// Creates a [SettingsLocationRoute].\n  const SettingsLocationRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsLocationPage());\n  }\n}\n\n/// Settings location select route - displays location selection page.\nclass SettingsLocationSelectRoute extends GoRouteData with $SettingsLocationSelectRoute {\n  /// Creates a [SettingsLocationSelectRoute].\n  const SettingsLocationSelectRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsLocationSelectPage());\n  }\n}\n\n/// Settings location select city route - displays city selection page.\nclass SettingsLocationSelectCityRoute extends GoRouteData with $SettingsLocationSelectCityRoute {\n  /// Creates a [SettingsLocationSelectCityRoute].\n  const SettingsLocationSelectCityRoute({required this.city});\n\n  /// The city parameter from the route path.\n  final String city;\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return Material(child: SettingsLocationSelectCityPage(city: city));\n  }\n}\n\nclass SettingsLayoutRoute extends GoRouteData with $SettingsLayoutRoute {\n  /// Creates a [SettingsLayoutRoute].\n  const SettingsLayoutRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsLayoutPage());\n  }\n}\n\n/// Settings theme route - displays theme settings.\nclass SettingsThemeRoute extends GoRouteData with $SettingsThemeRoute {\n  /// Creates a [SettingsThemeRoute].\n  const SettingsThemeRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsThemePage());\n  }\n}\n\n/// Settings theme mode route - displays theme mode selection page.\nclass SettingsThemeModeRoute extends GoRouteData with $SettingsThemeModeRoute {\n  /// Creates a [SettingsThemeModeRoute].\n  const SettingsThemeModeRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsThemeModePage());\n  }\n}\n\n/// Settings theme color route - displays theme color page.\nclass SettingsThemeColorRoute extends GoRouteData with $SettingsThemeColorRoute {\n  /// Creates a [SettingsThemeColorRoute].\n  const SettingsThemeColorRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsThemeColorPage());\n  }\n}\n\n/// Settings locale route - displays locale/language settings.\nclass SettingsLocaleRoute extends GoRouteData with $SettingsLocaleRoute {\n  /// Creates a [SettingsLocaleRoute].\n  const SettingsLocaleRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsLocalePage());\n  }\n}\n\n/// Settings locale select route - displays locale selection page.\nclass SettingsLocaleSelectRoute extends GoRouteData with $SettingsLocaleSelectRoute {\n  /// Creates a [SettingsLocaleSelectRoute].\n  const SettingsLocaleSelectRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsLocaleSelectPage());\n  }\n}\n\n/// Settings unit route - displays unit settings.\nclass SettingsUnitRoute extends GoRouteData with $SettingsUnitRoute {\n  /// Creates a [SettingsUnitRoute].\n  const SettingsUnitRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsUnitPage());\n  }\n}\n\n/// Settings map route - displays map settings.\nclass SettingsMapRoute extends GoRouteData with $SettingsMapRoute {\n  /// Creates a [SettingsMapRoute].\n  const SettingsMapRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsMapPage());\n  }\n}\n\n/// Settings proxy route - displays HTTP proxy settings.\nclass SettingsProxyRoute extends GoRouteData with $SettingsProxyRoute {\n  /// Creates a [SettingsProxyRoute].\n  const SettingsProxyRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsProxyPage());\n  }\n}\n\n/// Settings experimental route - displays experimental features settings.\nclass SettingsExperimentalRoute extends GoRouteData with $SettingsExperimentalRoute {\n  /// Creates a [SettingsExperimentalRoute].\n  const SettingsExperimentalRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsExperimentalPage());\n  }\n}\n\n/// Settings notify route - displays notification settings.\nclass SettingsNotifyRoute extends GoRouteData with $SettingsNotifyRoute {\n  /// Creates a [SettingsNotifyRoute].\n  const SettingsNotifyRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyPage());\n  }\n}\n\n/// Settings notify EEW route - displays earthquake early warning notification settings.\nclass SettingsNotifyEewRoute extends GoRouteData with $SettingsNotifyEewRoute {\n  /// Creates a [SettingsNotifyEewRoute].\n  const SettingsNotifyEewRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyEewPage());\n  }\n}\n\n/// Settings notify monitor route - displays seismic monitor notification settings.\nclass SettingsNotifyMonitorRoute extends GoRouteData with $SettingsNotifyMonitorRoute {\n  /// Creates a [SettingsNotifyMonitorRoute].\n  const SettingsNotifyMonitorRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyMonitorPage());\n  }\n}\n\n/// Settings notify report route - displays earthquake report notification settings.\nclass SettingsNotifyReportRoute extends GoRouteData with $SettingsNotifyReportRoute {\n  /// Creates a [SettingsNotifyReportRoute].\n  const SettingsNotifyReportRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyReportPage());\n  }\n}\n\n/// Settings notify intensity route - displays intensity notification settings.\nclass SettingsNotifyIntensityRoute extends GoRouteData with $SettingsNotifyIntensityRoute {\n  /// Creates a [SettingsNotifyIntensityRoute].\n  const SettingsNotifyIntensityRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyIntensityPage());\n  }\n}\n\n/// Settings notify thunderstorm route - displays thunderstorm notification settings.\nclass SettingsNotifyThunderstormRoute extends GoRouteData with $SettingsNotifyThunderstormRoute {\n  /// Creates a [SettingsNotifyThunderstormRoute].\n  const SettingsNotifyThunderstormRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyThunderstormPage());\n  }\n}\n\n/// Settings notify advisory route - displays weather advisory notification settings.\nclass SettingsNotifyAdvisoryRoute extends GoRouteData with $SettingsNotifyAdvisoryRoute {\n  /// Creates a [SettingsNotifyAdvisoryRoute].\n  const SettingsNotifyAdvisoryRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyAdvisoryPage());\n  }\n}\n\n/// Settings notify evacuation route - displays evacuation notification settings.\nclass SettingsNotifyEvacuationRoute extends GoRouteData with $SettingsNotifyEvacuationRoute {\n  /// Creates a [SettingsNotifyEvacuationRoute].\n  const SettingsNotifyEvacuationRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyEvacuationPage());\n  }\n}\n\n/// Settings notify tsunami route - displays tsunami notification settings.\nclass SettingsNotifyTsunamiRoute extends GoRouteData with $SettingsNotifyTsunamiRoute {\n  /// Creates a [SettingsNotifyTsunamiRoute].\n  const SettingsNotifyTsunamiRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyTsunamiPage());\n  }\n}\n\n/// Settings notify announcement route - displays announcement notification settings.\nclass SettingsNotifyAnnouncementRoute extends GoRouteData with $SettingsNotifyAnnouncementRoute {\n  /// Creates a [SettingsNotifyAnnouncementRoute].\n  const SettingsNotifyAnnouncementRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsNotifyAnnouncementPage());\n  }\n}\n\n/// Settings donate route - displays donation/support page.\nclass SettingsDonateRoute extends GoRouteData with $SettingsDonateRoute {\n  /// Creates a [SettingsDonateRoute].\n  const SettingsDonateRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const Material(child: SettingsDonatePage());\n  }\n}\n\n/// Map route - displays the map page with optional layers and report.\n@TypedGoRoute<MapRoute>(path: '/map')\nclass MapRoute extends GoRouteData with $MapRoute {\n  /// Creates a [MapRoute].\n  const MapRoute({this.layers, this.report});\n\n  /// Optional comma-separated list of map layers to display.\n  final String? layers;\n\n  /// Optional report ID to display on the map.\n  final String? report;\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return MapPage(\n      options: MapPageOptions.fromQueryParameters({\n        if (layers != null) 'layers': layers!,\n        if (report != null) 'report': report!,\n      }),\n    );\n  }\n}\n\n/// Announcement route - displays announcements page.\n@TypedGoRoute<AnnouncementRoute>(path: '/announcement')\nclass AnnouncementRoute extends GoRouteData with $AnnouncementRoute {\n  /// Creates an [AnnouncementRoute].\n  const AnnouncementRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const AnnouncementPage();\n  }\n}\n\n/// Changelog route - displays app changelog.\n@TypedGoRoute<ChangelogRoute>(path: '/changelog')\nclass ChangelogRoute extends GoRouteData with $ChangelogRoute {\n  /// Creates a [ChangelogRoute].\n  const ChangelogRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const ChangelogPage();\n  }\n}\n\n/// License route - displays open source licenses.\n@TypedGoRoute<LicenseRoute>(path: '/license')\nclass LicenseRoute extends GoRouteData with $LicenseRoute {\n  /// Creates a [LicenseRoute].\n  const LicenseRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const LicensePage();\n  }\n}\n\n/// Debug logs route - displays application debug logs.\n@TypedGoRoute<AppDebugLogsRoute>(path: '/debug/logs')\nclass AppDebugLogsRoute extends GoRouteData with $AppDebugLogsRoute {\n  /// Creates an [AppDebugLogsRoute].\n  const AppDebugLogsRoute();\n\n  @override\n  Widget build(BuildContext context, GoRouterState state) {\n    return const AppDebugLogsPage();\n  }\n}\n\n/// The main application router configured with all typed routes.\nfinal router = GoRouter(\n  navigatorKey: _rootNavigatorKey,\n  redirect: (context, state) {\n    // Handle initial location logic\n    if (state.matchedLocation == '/' || state.matchedLocation.isEmpty) {\n      if (Preference.isFirstLaunch) {\n        return const WelcomeRoute().location;\n      }\n      // Experimental: Launch to monitor if enabled\n      if (Preference.experimentalLaunchToMonitor == true) {\n        return const MapRoute(layers: 'monitor').location;\n      }\n      return const HomeRoute().location;\n    }\n    return null;\n  },\n  routes: $appRoutes,\n  observers: [TalkerRouteObserver(TalkerManager.instance), routeObserver],\n  debugLogDiagnostics: true,\n);\n"
  },
  {
    "path": "lib/utils/constants.dart",
    "content": "import 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Page transitions theme that uses zoom transitions with predictive back gesture support on Android.\n///\n/// This theme provides platform-specific page transitions:\n/// - iOS: Uses the standard Cupertino page transitions\n/// - Android: Uses predictive back page transitions for better gesture navigation support\nconst kZoomPageTransitionsTheme = PageTransitionsTheme(\n  builders: {\n    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),\n    TargetPlatform.android: PredictiveBackFullscreenPageTransitionsBuilder(),\n  },\n);\n\n/// Page transitions theme that uses fade-forward transitions with predictive back gesture support on Android.\n///\n/// This theme provides platform-specific page transitions:\n/// - iOS: Uses the standard Cupertino page transitions\n/// - Android: Uses predictive back fade-forward page transitions for a smoother fade effect\nconst kFadeForwardPageTransitionsTheme = PageTransitionsTheme(\n  builders: {\n    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),\n    TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),\n  },\n);\n\n/// Animation style that uses emphasized decelerate easing for smooth, natural-feeling animations.\n///\n/// This animation style uses Material Design 3's emphasized decelerate easing curve with medium duration\n/// for forward animations and short duration for reverse animations. The emphasized curve provides a more\n/// natural, physics-based animation feel compared to standard easing curves.\nconst kEmphasizedAnimationStyle = AnimationStyle(\n  curve: Easing.emphasizedDecelerate,\n  duration: Durations.medium4,\n  reverseCurve: Easing.emphasizedDecelerate,\n  reverseDuration: Durations.short4,\n);\n\n/// Duration for persistent snackbars that should remain visible for an extended period.\n///\n/// This duration is set to 365 days, effectively making snackbars persist until manually dismissed.\n/// This is useful for displaying important information or error messages that users should acknowledge\n/// before they disappear.\nconst kPersistSnackBar = Duration(days: 365);\n\n/// MapLibre expression for symbol icon size that scales with zoom level.\n///\n/// This expression defines a linear interpolation for icon size based on map zoom level:\n/// - At zoom level 5: icon size is 0.2\n/// - At zoom level 15: icon size is 0.6\n/// - Between zoom levels 5 and 15: size interpolates linearly\n///\n/// This ensures that symbol icons remain appropriately sized as users zoom in and out of the map.\nconst kSymbolIconSize = [\n  Expressions.interpolate,\n  ['linear'],\n  [Expressions.zoom],\n  5,\n  0.2,\n  15,\n  0.6,\n];\n\n/// MapLibre expression for circle icon size that scales with zoom level.\n///\n/// This expression defines a linear interpolation for circle radius based on map zoom level:\n/// - At zoom level 5: circle radius is 2\n/// - At zoom level 15: circle radius is 12\n/// - Between zoom levels 5 and 15: radius interpolates linearly\n///\n/// This ensures that circle markers remain appropriately sized as users zoom in and out of the map.\nconst kCircleIconSize = [\n  Expressions.interpolate,\n  ['linear'],\n  [Expressions.zoom],\n  5,\n  2,\n  15,\n  12,\n];\n"
  },
  {
    "path": "lib/utils/depth_color.dart",
    "content": "import 'dart:ui';\n\nconst kDepthColor0 = Color(0xFF000000);\nconst kDepthColor1 = Color(0xFFFF0000);\nconst kDepthColor2 = Color(0xFFFF6400);\nconst kDepthColor3 = Color(0xFFFFC800);\nconst kDepthColor4 = Color(0xFF00C800);\nconst kDepthColor5 = Color(0xFF00C8C8);\nconst kDepthColor6 = Color(0xFF0000C8);\n\nColor getDepthColor(double depth) {\n  final depthList = [5, 15, 30, 50, 100, 150];\n  final colorList = [\n    kDepthColor1,\n    kDepthColor2,\n    kDepthColor3,\n    kDepthColor4,\n    kDepthColor5,\n    kDepthColor6,\n  ];\n\n  if (depth <= depthList.first) return colorList.first;\n\n  if (depth >= depthList.last) return colorList.last;\n\n  for (int i = 0; i < depthList.length - 1; i++) {\n    if (depth >= depthList[i] && depth < depthList[i + 1]) {\n      final double localT = (depth - depthList[i]) / (depthList[i + 1] - depthList[i]);\n      return Color.lerp(colorList[i], colorList[i + 1], localT)!;\n    }\n  }\n  return kDepthColor0;\n}\n"
  },
  {
    "path": "lib/utils/extensions/asset_bundle.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/services.dart';\n\n/// Extension on [AssetBundle] that provides convenient utilities for loading asset files.\n///\n/// This extension adds helpful methods to simplify loading and parsing asset files, particularly JSON files that are\n/// commonly used for configuration data and static content.\nextension AssetBundleExtension on AssetBundle {\n  /// Loads a JSON file from the asset bundle and parses it as a map.\n  ///\n  /// The [path] parameter specifies the asset path relative to the `assets` directory in `pubspec.yaml`. The file is\n  /// loaded as a string, then parsed using `jsonDecode` and cast to `Map<String, dynamic>`.\n  ///\n  /// Example:\n  /// ```dart\n  /// final data = await rootBundle.loadJson('assets/config.json');\n  /// final value = data['key'];\n  /// ```\n  ///\n  /// Throws an exception if the file cannot be loaded or if the JSON is invalid.\n  Future<Map<String, dynamic>> loadJson(String path) async {\n    final json = await loadString(path);\n    return jsonDecode(json) as Map<String, dynamic>;\n  }\n}\n"
  },
  {
    "path": "lib/utils/extensions/build_context.dart",
    "content": "import 'package:dpip/utils/extensions/go_router.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\n/// Extension on [BuildContext] that provides convenient access to commonly used Flutter framework objects and\n/// utilities.\n///\n/// This extension simplifies access to theme data, media query information, navigation, and other frequently used\n/// context-dependent objects, reducing boilerplate code throughout the application.\n///\n/// Example usage:\n/// ```dart\n/// // Instead of: Theme.of(context)\n/// context.theme\n///\n/// // Instead of: MediaQuery.sizeOf(context)\n/// context.dimension\n///\n/// // Instead of: Navigator.of(context)\n/// context.navigator\n/// ```\nextension BuildContextExtension on BuildContext {\n  /// Returns the [ThemeData] from the nearest [Theme] ancestor.\n  ThemeData get theme => Theme.of(this);\n\n  /// Returns the [ColorScheme] from the current theme.\n  ColorScheme get colors => theme.colorScheme;\n\n  /// Returns the [TextTheme] from the current theme.\n  TextTheme get texts => theme.textTheme;\n\n  /// Returns the size of the current media (screen/window).\n  Size get dimension => MediaQuery.sizeOf(this);\n\n  /// Returns the padding insets of the current media (e.g., system UI insets).\n  EdgeInsets get padding => MediaQuery.paddingOf(this);\n\n  /// Returns the brightness of the current platform\n  Brightness get brightness => MediaQuery.platformBrightnessOf(this);\n\n  /// Returns the [NavigatorState] from the nearest [Navigator] ancestor.\n  NavigatorState get navigator => Navigator.of(this);\n\n  /// Returns the [ScaffoldMessengerState] from the nearest [ScaffoldMessenger] ancestor.\n  ScaffoldMessengerState get scaffoldMessenger => ScaffoldMessenger.of(this);\n\n  /// Returns the [GoRouter] instance from the current context.\n  ///\n  /// Example:\n  /// ```dart\n  /// // Navigate to a new route\n  /// context.router.push('/settings');\n  ///\n  /// // Get the current route location\n  /// final location = context.router.routerDelegate.currentConfiguration.uri.toString();\n  /// ```\n  GoRouter get router => GoRouter.of(this);\n\n  /// Pops the navigation stack until reaching the route matching [path].\n  ///\n  /// This method uses [GoRouter] to pop routes from the navigation stack until it finds a route matching [path]. If the\n  /// path is not found in the current navigation stack, the method will stop at the root route.\n  ///\n  /// The [path] should match the route path defined in your router configuration.\n  ///\n  /// Example:\n  /// ```dart\n  /// // Pop until reaching the settings page\n  /// context.popUntil('/settings');\n  /// ```\n  void popUntil(String path) => router.popUntil(path);\n\n  /// Returns the height and width constraints for bottom sheets in Material 3.\n  ///\n  /// These constraints follow Material Design 3 guidelines for bottom sheets:\n  /// - Maximum height: the dimension's height minus 72 logical pixels (to account for system UI and spacing)\n  /// - Maximum width: 640 logical pixels (standard maximum width for bottom sheets)\n  ///\n  /// Example:\n  /// ```dart\n  /// showModalBottomSheet(\n  ///   context: context,\n  ///   constraints: context.bottomSheetConstraints,\n  ///   builder: (context) => MyBottomSheet(),\n  /// );\n  /// ```\n  BoxConstraints get bottomSheetConstraints =>\n      BoxConstraints(maxHeight: dimension.height - 72, maxWidth: 640);\n}\n"
  },
  {
    "path": "lib/utils/extensions/color.dart",
    "content": "import 'dart:ui';\n\n/// Extension on [Color] that provides color manipulation utilities.\n///\n/// This extension adds helpful methods and getters for transforming and manipulating colors, including color inversion\n/// and other common color operations.\nextension ColorExtension on Color {\n  /// Gets the inverted color of this color.\n  ///\n  /// Inverts the red, green, and blue channels by subtracting each component from 1.0, while preserving the original\n  /// alpha (opacity) value. This creates a complementary color effect commonly used for contrast or visual effects.\n  ///\n  /// The formula for each channel is: `inverted = 1.0 - original`\n  ///\n  /// Example:\n  /// ```dart\n  /// final white = Color(0xFFFFFFFF);\n  /// final black = white.inverted; // Color(0xFF000000)\n  ///\n  /// final red = Color(0xFFFF0000);\n  /// final cyan = red.inverted; // Color(0xFF00FFFF)\n  /// ```\n  Color get inverted => Color.from(\n    alpha: a,\n    red: 1 - r,\n    green: 1 - g,\n    blue: 1 - b,\n  );\n}\n"
  },
  {
    "path": "lib/utils/extensions/color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ExtendedColors {\n  late Color blue;\n  late Color onBlue;\n  late Color blueContainer;\n  late Color onBlueContainer;\n  late Color green;\n  late Color onGreen;\n  late Color greenContainer;\n  late Color onGreenContainer;\n  late Color amber;\n  late Color onAmber;\n  late Color amberContainer;\n  late Color onAmberContainer;\n  late Color grey;\n  late Color onGrey;\n  late Color greyContainer;\n  late Color onGreyContainer;\n  late Color brown;\n  late Color onBrown;\n  late Color brownContainer;\n  late Color onBrownContainer;\n\n  ExtendedColors(Brightness brightness) {\n    final blueScheme = ColorScheme.fromSeed(\n      seedColor: Colors.blueAccent,\n      brightness: brightness,\n    );\n\n    blue = blueScheme.primary;\n    onBlue = blueScheme.onPrimary;\n    blueContainer = blueScheme.primaryContainer;\n    onBlueContainer = blueScheme.onPrimaryContainer;\n\n    final greenScheme = ColorScheme.fromSeed(\n      seedColor: Colors.green,\n      brightness: brightness,\n    );\n\n    green = greenScheme.primary;\n    onGreen = greenScheme.onPrimary;\n    greenContainer = greenScheme.primaryContainer;\n    onGreenContainer = greenScheme.onPrimaryContainer;\n\n    final amberScheme = ColorScheme.fromSeed(\n      seedColor: Colors.amber,\n      brightness: brightness,\n    );\n\n    amber = amberScheme.primary;\n    onAmber = amberScheme.onPrimary;\n    amberContainer = amberScheme.primaryContainer;\n    onAmberContainer = amberScheme.onPrimaryContainer;\n\n    final greyScheme = ColorScheme.fromSeed(\n      seedColor: Colors.grey,\n      brightness: brightness,\n      dynamicSchemeVariant: DynamicSchemeVariant.neutral,\n    );\n\n    grey = greyScheme.primary;\n    onGrey = greyScheme.onPrimary;\n    greyContainer = greyScheme.primaryContainer;\n    onGreyContainer = greyScheme.onPrimaryContainer;\n\n    final brownScheme = ColorScheme.fromSeed(\n      seedColor: Colors.brown,\n      brightness: brightness,\n    );\n\n    brown = brownScheme.primary;\n    onBrown = brownScheme.onPrimary;\n    brownContainer = brownScheme.primaryContainer;\n    onBrownContainer = brownScheme.onPrimaryContainer;\n  }\n}\n\n/// Extension on [ThemeData] that provides convenient access to extended semantic colors.\n///\n/// This extension adds helpful getters to access extended color palettes that complement the standard Material Design 3\n/// color scheme.\nextension CustomColors on ThemeData {\n  /// Returns an [ExtendedColors] instance with colors matching the current theme's brightness.\n  ExtendedColors get extendedColors => ExtendedColors(brightness);\n}\n"
  },
  {
    "path": "lib/utils/extensions/datetime.dart",
    "content": "import 'package:dpip/core/i18n.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:i18n_extension/i18n_extension.dart';\nimport 'package:intl/intl.dart';\nimport 'package:timezone/timezone.dart';\n\n/// Cache for [DateFormat] instances to avoid recreating them for the same pattern and locale.\nfinal Map<String, DateFormat> _dateFormatCache = {};\n\n/// Gets or creates a [DateFormat] instance for the given [pattern] and optional [locale].\n///\n/// This function implements a caching mechanism to reuse [DateFormat] instances, improving performance when formatting\n/// multiple dates with the same pattern and locale.\n///\n/// The [pattern] is first translated via i18n (if a translation exists), then used to create or retrieve a cached\n/// [DateFormat] instance. The cache key is constructed from the translated pattern and locale identifier to ensure\n/// proper caching even when patterns differ by locale.\nDateFormat _getDateFormat(String pattern, [String? locale]) {\n  // Translate the pattern via i18n first\n  final translatedPattern = pattern.i18n;\n\n  // Construct cache key from translated pattern and locale\n  final key = locale != null ? '$translatedPattern-$locale' : translatedPattern;\n\n  return _dateFormatCache.putIfAbsent(\n    key,\n    () => DateFormat(translatedPattern, locale),\n  );\n}\n\n/// Extension on [DateTime] that provides convenient utilities for formatting dates and times.\n///\n/// This extension adds helpful methods and getters to simplify date and time formatting operations, including\n/// locale-aware formatting and standardized string representations.\nextension DateTimeExtension on DateTime {\n  /// Formats this date and time as a simple string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm\" (e.g., \"12/25 14:30\").\n  String toSimpleDateTimeString() => _getDateFormat('MM/dd HH:mm').format(this);\n\n  /// Formats this date as a locale-aware full date string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd (EEEE)\" (e.g., \"2024/12/25 (Wednesday)\"). The day of the week is\n  /// localized according to [context]'s locale.\n  String toLocaleFullDateString(BuildContext context) => _getDateFormat(\n    'yyyy/MM/dd (EEEE)',\n    context.locale.toLanguageTag(),\n  ).format(this);\n\n  /// Formats this date and time as a full string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd HH:mm:ss\" (e.g., \"2024/12/25 14:30:45\").\n  String toDateTimeString() => _getDateFormat('yyyy/MM/dd HH:mm:ss').format(this);\n\n  /// Formats the time portion of this date as a string.\n  ///\n  /// Returns a string in the format \"HH:mm:ss\" (e.g., \"14:30:45\").\n  String toLocaleTimeString() => _getDateFormat('HH:mm:ss').format(this);\n\n  /// Formats this date and time as a full simple string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm:ss\" (e.g., \"12/25 14:30:45\").\n  String toFullSimpleDateTimeString() => _getDateFormat('MM/dd HH:mm:ss').format(this);\n}\n\n/// Extension on [TZDateTime] that provides convenient utilities for formatting timezone-aware dates and times.\n///\n/// This extension adds helpful methods and getters to simplify timezone-aware date and time formatting operations,\n/// including locale-aware formatting and standardized string representations.\nextension TZDateTimeExtension on TZDateTime {\n  /// Formats this date and time as a simple string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm\" (e.g., \"12/25 14:30\").\n  String toSimpleDateTimeString() => _getDateFormat('MM/dd HH:mm').format(this);\n\n  /// Formats this date as a locale-aware full date string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd (EEEE)\" (e.g., \"2024/12/25 (Wednesday)\"). The day of the week is\n  /// localized according to [context]'s locale.\n  String toLocaleFullDateString(BuildContext context) => _getDateFormat(\n    'yyyy/MM/dd (EEEE)',\n    context.locale.toLanguageTag(),\n  ).format(this);\n\n  /// Formats this date and time as a locale-aware full string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd HH:mm:ss\" (e.g., \"2024/12/25 14:30:45\"). The format is localized\n  /// according to [context]'s locale.\n  String toLocaleDateTimeString(BuildContext context) => _getDateFormat(\n    'yyyy/MM/dd HH:mm:ss',\n    context.locale.toLanguageTag(),\n  ).format(this);\n\n  /// Formats the time portion of this date as a locale-aware string.\n  ///\n  /// Returns a string in the format \"HH:mm:ss\" (e.g., \"14:30:45\"). The format is localized according to [context]'s\n  /// locale.\n  String toLocaleTimeString(BuildContext context) =>\n      _getDateFormat('HH:mm:ss', context.locale.toLanguageTag()).format(this);\n\n  /// Formats this date and time as a full simple string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm:ss\" (e.g., \"12/25 14:30:45\").\n  String toFullSimpleDateTimeString() => _getDateFormat('MM/dd HH:mm:ss').format(this);\n}\n"
  },
  {
    "path": "lib/utils/extensions/go_router.dart",
    "content": "import 'package:go_router/go_router.dart';\n\n/// Extension on [GoRouter] that provides convenient utilities for navigation operations.\n///\n/// This extension adds helpful methods to simplify common navigation patterns and route management operations that are\n/// not directly available in the standard GoRouter API.\nextension GoRouterExtension on GoRouter {\n  /// Pops routes from the navigation stack until a route matching [routePath] is reached.\n  ///\n  /// This method traverses the current route stack from top to bottom, removing routes until it finds a route whose\n  /// path ends with [routePath]. If the target route is found, navigation stops at that route. If the route is not\n  /// found in the stack, the method will continue popping until the root route is reached.\n  ///\n  /// The method handles [ShellRoute] instances specially by using the `restore` method to properly manage nested\n  /// navigation stacks, ensuring that shell routes are correctly handled during the pop operation.\n  ///\n  /// The [routePath] parameter is a path suffix to match against route paths. The method uses `endsWith` to match\n  /// routes, so partial paths are supported (e.g., '/settings' will match '/app/settings').\n  ///\n  /// This method only processes [GoRoute] instances and skips other route types in the stack.\n  ///\n  /// Example:\n  /// ```dart\n  /// // Pop until reaching the settings page\n  /// GoRouter.of(context).popUntil('/settings');\n  ///\n  /// // Or using the BuildContext extension\n  /// context.popUntil('/settings');\n  /// ```\n  void popUntil(String routePath) {\n    final routerDelegate = this.routerDelegate;\n    final routeStacks = routerDelegate.currentConfiguration.routes.toList();\n\n    for (int i = routeStacks.length - 1; i >= 0; i--) {\n      final route = routeStacks[i];\n\n      if (route is! GoRoute) continue;\n      if (route.path.endsWith(routePath)) break;\n\n      if (i != 0 && routeStacks[i - 1] is ShellRoute) {\n        final matchList = routerDelegate.currentConfiguration;\n        restore(matchList.remove(matchList.matches.last));\n      } else {\n        pop();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/extensions/iterable.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Extension on [Iterable] that provides convenient utilities for working with iterable collections.\n///\n/// This extension adds helpful methods to simplify common operations on iterable collections, including ordering,\n/// searching, and element access.\nextension IterableExtension<T> on Iterable<T> {\n  /// Orders this iterable according to the sequence specified in [order].\n  ///\n  /// Returns a new iterable with elements sorted according to their position in [order]. Elements that appear in\n  /// [order] are sorted by their index in [order], while elements not found in [order] are placed at the end,\n  /// maintaining their relative order.\n  ///\n  /// The [order] parameter defines the desired ordering sequence. Elements are compared by their position in this\n  /// sequence using `indexOf`, so elements appearing earlier in [order] will appear earlier in the result.\n  ///\n  /// Example:\n  /// ```dart\n  /// final items = ['c', 'a', 'd', 'b'];\n  /// final order = ['a', 'b', 'c'];\n  /// final ordered = items.orderedBy(order); // ['a', 'b', 'c', 'd']\n  /// ```\n  ///\n  /// Note: Elements not present in [order] will have an index of -1 and will be sorted to the end.\n  Iterable<T> orderedBy(Iterable<T> order) {\n    final orderList = order.toList();\n    return toList().sorted(\n      (a, b) => orderList.indexOf(a) - orderList.indexOf(b),\n    );\n  }\n\n  /// Checks whether any element of this iterable satisfies [test].\n  ///\n  /// Checks every element in iteration order, and returns `true` if any element matches [test] and `false` otherwise.\n  ///\n  /// Example:\n  /// ```dart\n  /// final list = [1, 2, 3];\n  /// final result = list.containsWhere((element) => element == 2); // true\n  /// ```\n  bool containsWhere(bool Function(T element) test) {\n    for (final element in this) {\n      if (test(element)) return true;\n    }\n    return false;\n  }\n\n  /// Returns the last element of this iterable, or `null` if the iterable is empty.\n  T? get lastOrNull => isNotEmpty ? last : null;\n}\n\n/// Extension on [List] that provides convenient utilities for working with lists.\n///\n/// This extension adds helpful methods to simplify common list operations, such as joining elements with separators.\nextension ListExtension<T> on List<T> {\n  /// Joins the elements of this list with the given [separator] between each element.\n  ///\n  /// This is similar to [List.join] but returns a new list instead of a string. The resulting list will have `length *\n  /// 2 - 1` elements, with the [separator] inserted between each original element.\n  ///\n  /// Returns an empty list if this list is empty.\n  ///\n  /// Example:\n  /// ```dart\n  /// final list = [1, 2, 3];\n  /// final result = list.superJoin(0); // [1, 0, 2, 0, 3]\n  /// ```\n  List<T> superJoin(T separator) {\n    if (isEmpty) return [];\n    return expand(\n      (element) => [element, separator],\n    ).take(length * 2 - 1).toList();\n  }\n}\n\n/// Extension on [List<double>] that provides convenient utilities for geographic coordinate conversion.\n///\n/// This extension adds helpful getters and methods to convert lists of doubles to geographic coordinate objects and\n/// perform geographic operations used in mapping libraries.\nextension ListDoubleExtension on List<double> {\n  /// Converts this list to a [LatLng] coordinate.\n  ///\n  /// The list must contain at least 2 elements: `[latitude, longitude]`.\n  LatLng get asLatLng => LatLng(this[0], this[1]);\n\n  /// Converts this list to a [LatLngBounds] object.\n  ///\n  /// The list must contain at least 4 elements: `[southwestLat, southwestLng, northeastLat, northeastLng]`.\n  LatLngBounds get asLatLngBounds => LatLngBounds(\n    southwest: LatLng(this[0], this[1]),\n    northeast: LatLng(this[2], this[3]),\n  );\n\n  /// Expands this bounding box to include the given [point].\n  ///\n  /// This list must contain exactly 4 elements representing a bounding box in the format\n  /// `[southLat, westLng, northLat, eastLng]`. The method modifies this list in place to include\n  /// the given [point] within the bounds, expanding the box as necessary.\n  ///\n  /// Returns this list for method chaining.\n  ///\n  /// Example:\n  /// ```dart\n  /// final bounds = [25.0, 121.0, 25.5, 121.5]; // Initial bounds\n  /// bounds.expandBounds(LatLng(24.9, 120.9)); // Expands south and west\n  /// ```\n  List<double> expandBounds(LatLng point) {\n    assert(length == 4, 'Bounds must contain exactly 4 elements');\n\n    // South\n    if (this[0] > point.latitude) {\n      this[0] = point.latitude;\n    }\n    // West\n    if (this[1] > point.longitude) {\n      this[1] = point.longitude;\n    }\n    // North\n    if (this[2] < point.latitude) {\n      this[2] = point.latitude;\n    }\n    // East\n    if (this[3] < point.longitude) {\n      this[3] = point.longitude;\n    }\n\n    return this;\n  }\n}\n\n/// Extension on [Set] that provides convenient utilities for ordering collections.\n///\n/// This extension adds helpful methods to simplify ordering operations based on a predefined order sequence, making it\n/// easy to sort collections according to a specific order rather than natural ordering.\nextension SetExtension<T> on Set<T> {\n  /// Orders this set according to the sequence specified in [order].\n  ///\n  /// Returns a new set with elements sorted according to their position in [order]. Elements that appear in [order] are\n  /// sorted by their index in [order], while elements not found in [order] are placed at the end, maintaining their\n  /// relative order.\n  ///\n  /// The [order] parameter defines the desired ordering sequence. Elements are compared by their position in this\n  /// sequence using `indexOf`, so elements appearing earlier in [order] will appear earlier in the result.\n  ///\n  /// Example:\n  /// ```dart\n  /// final items = {'c', 'a', 'd', 'b'};\n  /// final order = ['a', 'b', 'c'];\n  /// final ordered = items.orderedBy(order); // {'a', 'b', 'c', 'd'}\n  /// ```\n  ///\n  /// Note: Elements not present in [order] will have an index of -1 and will be sorted to the end.\n  Set<T> orderedBy(Iterable<T> order) {\n    final orderList = order.toList();\n    return sorted(\n      (a, b) => orderList.indexOf(a) - orderList.indexOf(b),\n    ).toSet();\n  }\n}\n"
  },
  {
    "path": "lib/utils/extensions/latlng.dart",
    "content": "import 'package:dpip/utils/geojson.dart';\nimport 'package:geolocator/geolocator.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Extension on [LatLng] that provides convenient utilities for GeoJSON conversion and geographic operations.\n///\n/// This extension adds helpful methods and getters to simplify GeoJSON formatting, coordinate validation,\n/// and distance calculations for geographic coordinates.\nextension GeoJsonLatLng on LatLng {\n  /// Checks whether this coordinate is valid.\n  ///\n  /// Returns `true` if both latitude and longitude are non-zero, `false` otherwise.\n  /// This is useful for filtering out invalid or uninitialized coordinates.\n  bool get isValid => latitude != 0 && longitude != 0;\n\n  /// Converts this coordinate to a GeoJSON coordinate array.\n  ///\n  /// Returns a list in the format `[longitude, latitude]` as required by the GeoJSON specification.\n  List<double> get asGeoJsonCooridnate => [longitude, latitude];\n\n  /// Converts this coordinate to a GeoJSON feature builder.\n  ///\n  /// Returns a [GeoJsonFeatureBuilder] configured as a Point feature with this coordinate's geometry.\n  /// The builder can be further customized with properties before building the final GeoJSON feature.\n  GeoJsonFeatureBuilder toGeoJsonFeatureBuilder() {\n    return GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)..setGeometry(asGeoJsonCooridnate);\n  }\n\n  /// Converts this coordinate to a GeoJSON builder.\n  ///\n  /// Returns a [GeoJsonBuilder] containing a single Point feature representing this coordinate.\n  /// The builder can be used to add additional features or build a complete GeoJSON FeatureCollection.\n  GeoJsonBuilder toGeoJsonBuilder() {\n    return GeoJsonBuilder()..addFeature(toGeoJsonFeatureBuilder());\n  }\n\n  /// Converts this coordinate to a GeoJSON map.\n  ///\n  /// Returns a complete GeoJSON FeatureCollection map containing a single Point feature representing\n  /// this coordinate. The map is ready to be serialized to JSON.\n  Map<String, dynamic> toGeoJsonMap() {\n    return toGeoJsonBuilder().build();\n  }\n\n  /// Calculates the distance between this coordinate and [other] in meters.\n  ///\n  /// The distance is calculated using the Haversine formula, which accounts for the Earth's\n  /// spherical shape to provide accurate distance measurements between two geographic coordinates.\n  ///\n  /// See also:\n  /// - https://en.wikipedia.org/wiki/Haversine_formula for details on the Haversine formula\n  ///\n  /// Example:\n  /// ```dart\n  /// final point1 = LatLng(25.0330, 121.5654);\n  /// final point2 = LatLng(24.1477, 120.6736);\n  /// final distance = point1.to(point2);\n  /// ```\n  double to(LatLng other) => Geolocator.distanceBetween(\n    latitude,\n    longitude,\n    other.latitude,\n    other.longitude,\n  );\n}\n"
  },
  {
    "path": "lib/utils/extensions/locale.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// Extension on [Locale] that provides convenient utilities for locale display and formatting.\n///\n/// This extension adds helpful getters to simplify displaying locale information in the user interface, including\n/// native language names and compact icon labels for language selection.\nextension NativeLocale on Locale {\n  static const Map<String, String> _nativeNames = {\n    'zh-Hant': '繁體中文',\n    'zh-Hans': '簡體中文',\n    'en': 'English',\n    'ja': '日本語',\n    'ko': '한국어',\n    'vi': 'Tiếng Việt',\n    'ru': 'Русский',\n  };\n\n  static const Map<String, String> _iconLabels = {\n    'zh-Hant': '繁',\n    'zh-Hans': '简',\n    'en': 'EN',\n    'ja': 'あ',\n    'ko': '한',\n    'vi': 'VI',\n    'ru': 'РУ',\n  };\n\n  /// Returns the native name of this locale in its own language.\n  ///\n  /// Returns the language name written in the language itself (e.g., \"繁體中文\" for Traditional Chinese, \"English\" for\n  /// English, \"日本語\" for Japanese). This is useful for displaying language options in language selection interfaces\n  /// where users can recognize their preferred language.\n  ///\n  /// For unsupported locales, returns the string representation of the locale.\n  ///\n  /// Example:\n  /// ```dart\n  /// final locale = Locale('zh', 'TW');\n  /// print(locale.nativeName); // \"繁體中文\"\n  ///\n  /// final englishLocale = Locale('en');\n  /// print(englishLocale.nativeName); // \"English\"\n  /// ```\n  String get nativeName => _nativeNames[toLanguageTag()] ?? toString();\n\n  /// Returns a compact label suitable for use in icons or compact UI elements.\n  ///\n  /// Returns a short, typically 1-2 character label representing the locale (e.g., \"繁\" for Traditional Chinese, \"EN\"\n  /// for English, \"あ\" for Japanese). This is useful for displaying language indicators in compact spaces like icon\n  /// buttons or badges.\n  ///\n  /// For unsupported locales, returns the first 2 characters of the locale's string representation.\n  ///\n  /// Example:\n  /// ```dart\n  /// final locale = Locale('zh', 'TW');\n  /// print(locale.iconLabel); // \"繁\"\n  ///\n  /// final englishLocale = Locale('en');\n  /// print(englishLocale.iconLabel); // \"EN\"\n  /// ```\n  String get iconLabel => _iconLabels[toLanguageTag()] ?? toString().substring(0, 2);\n}\n"
  },
  {
    "path": "lib/utils/extensions/maplibre.dart",
    "content": "import 'package:dpip/widgets/map/map.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\n/// Extension on [MapLibreMapController] that provides convenient utilities for map layer and base map management.\n///\n/// This extension adds helpful methods to simplify common map operations, including base map switching, layer\n/// visibility control, and resource existence checking.\nextension MapLibreMapControllerExtension on MapLibreMapController {\n  /// Sets the base map type and updates the visibility of all base map layers accordingly.\n  ///\n  /// This method switches between different base map providers (OSM, Google, Exptech) by showing the selected base map\n  /// and hiding the others. All visibility changes are applied concurrently for better performance.\n  ///\n  /// Example:\n  /// ```dart\n  /// await controller.setBaseMap(BaseMapType.exptech);\n  /// ```\n  Future<void> setBaseMap(BaseMapType baseMapType) async {\n    await Future.wait([\n      setOSMVisibility(baseMapType == BaseMapType.osm),\n      setGoogleVisibility(baseMapType == BaseMapType.google),\n      setExptechVisibility(baseMapType == BaseMapType.exptech),\n    ]);\n  }\n\n  /// Sets the visibility of all OSM (OpenStreetMap) layers.\n  ///\n  /// Finds all layers with IDs starting with 'osm-' and sets their visibility to [visible]. All layer visibility\n  /// changes are applied concurrently for better performance.\n  ///\n  /// Example:\n  /// ```dart\n  /// await controller.setOSMVisibility(true); // Show all OSM layers\n  /// await controller.setOSMVisibility(false); // Hide all OSM layers\n  /// ```\n  Future<void> setOSMVisibility(bool visible) async {\n    final layers = (await getLayerIds()).cast<String>();\n    final osmLayers = layers.where((v) => v.startsWith('osm-'));\n\n    await Future.wait(osmLayers.map((v) => setLayerVisibility(v, visible)));\n  }\n\n  /// Sets the visibility of all Google Maps layers.\n  ///\n  /// Finds all layers with IDs starting with 'google-' and sets their visibility to [visible]. All layer visibility\n  /// changes are applied concurrently for better performance.\n  ///\n  /// Example:\n  /// ```dart\n  /// await controller.setGoogleVisibility(true); // Show all Google layers\n  /// await controller.setGoogleVisibility(false); // Hide all Google layers\n  /// ```\n  Future<void> setGoogleVisibility(bool visible) async {\n    final layers = (await getLayerIds()).cast<String>();\n    final googleLayers = layers.where((v) => v.startsWith('google-'));\n\n    await Future.wait(googleLayers.map((v) => setLayerVisibility(v, visible)));\n  }\n\n  /// Sets the visibility of all Exptech base map layers.\n  ///\n  /// Finds all layers with IDs starting with 'exptech-' and sets their visibility to [visible]. All layer visibility\n  /// changes are applied concurrently for better performance.\n  ///\n  /// Example:\n  /// ```dart\n  /// await controller.setExptechVisibility(true); // Show all Exptech layers\n  /// await controller.setExptechVisibility(false); // Hide all Exptech layers\n  /// ```\n  Future<void> setExptechVisibility(bool visible) async {\n    final layers = (await getLayerIds()).cast<String>();\n    final exptechLayers = layers.where((v) => v.startsWith('exptech-'));\n\n    await Future.wait(exptechLayers.map((v) => setLayerVisibility(v, visible)));\n  }\n\n  /// Checks if the provided [id] exists in the map as a source or layer.\n  ///\n  /// By default, checks both sources and layers. Use [source] and [layer] parameters to limit the search scope:\n  /// - If [source] is `true`, only sources will be checked\n  /// - If [layer] is `true`, only layers will be checked\n  /// - If both are `true`, both sources and layers will be checked\n  ///\n  /// Returns `true` if the [id] exists in any of the checked categories, otherwise, returns `false`.\n  ///\n  /// Example:\n  /// ```dart\n  /// // Check if a layer exists\n  /// final layerExists = await controller.exists('my-layer', layer: true);\n  ///\n  /// // Check if a source exists\n  /// final sourceExists = await controller.exists('my-source', source: true);\n  ///\n  /// // Check both (default behavior)\n  /// final exists = await controller.exists('my-resource');\n  /// ```\n  Future<bool> exists(String id, {bool? source, bool? layer}) async {\n    final shouldCheckBoth = source == null && layer == null;\n\n    final checkSource = shouldCheckBoth || (source ?? false);\n    final checkLayer = shouldCheckBoth || (layer ?? false);\n\n    if (checkSource) {\n      final sourceIds = await getSourceIds();\n      if (sourceIds.contains(id)) return true;\n    }\n\n    if (checkLayer) {\n      final layerIds = await getLayerIds();\n      if (layerIds.contains(id)) return true;\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/utils/extensions/number.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:flutter/material.dart';\nimport 'package:timezone/timezone.dart';\n\n/// Extension on [int] that provides convenient utilities for working with integer values.\n///\n/// This extension adds helpful methods and getters to simplify common operations on [int] values, such as earthquake\n/// intensity conversions, temperature unit conversions, and date-time formatting.\nextension IntExtension on int {\n  /// Converts this earthquake intensity value to its numeric string representation.\n  ///\n  /// Returns a string representation of the intensity level (0-7). Note that intensity levels 5 and 6 each map to two\n  /// values (5→'5', 6→'5', 7→'6', 8→'6', 9→'7') to account for the 5弱/5強 and 6弱/6強 subdivisions in the Japanese\n  /// intensity scale.\n  String get asIntensityNumber => ['0', '1', '2', '3', '4', '5', '5', '6', '6', '7'][this];\n\n  /// Converts this earthquake intensity value to its localized label.\n  ///\n  /// Returns a localized string label for the intensity level (e.g., \"０級\", \"５弱\", \"７級\"). The labels are translated\n  /// via i18n according to the current locale.\n  String get asIntensityLabel => [\n    '０級'.i18n,\n    '１級'.i18n,\n    '２級'.i18n,\n    '３級'.i18n,\n    '４級'.i18n,\n    '５弱'.i18n,\n    '５強'.i18n,\n    '６弱'.i18n,\n    '６強'.i18n,\n    '７級'.i18n,\n  ][this];\n\n  /// Converts this earthquake intensity value to its display label.\n  ///\n  /// Returns a compact string label suitable for display (e.g., \"0\", \"5⁻\", \"5⁺\", \"7\"). Uses superscript minus (⁻) and\n  /// plus (⁺) symbols for the 5弱/5強 and 6弱/6強 subdivisions.\n  String get asIntensityDisplayLabel =>\n      ['0', '1', '2', '3', '4', '5⁻', '5⁺', '6⁻', '6⁺', '7'][this];\n\n  /// Converts this timestamp (milliseconds since epoch) to a [TZDateTime].\n  ///\n  /// The timestamp is parsed and converted to a timezone-aware date-time object using the default timezone location\n  /// (typically 'Asia/Taipei').\n  TZDateTime get asTZDateTime {\n    final location = getLocation('Asia/Taipei');\n    return TZDateTime.fromMillisecondsSinceEpoch(location, this);\n  }\n\n  /// Formats this timestamp as a simple date-time string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm\" (e.g., \"12/25 14:30\").\n  String toSimpleDateTimeString() => asTZDateTime.toSimpleDateTimeString();\n\n  /// Formats this timestamp as a full simple date-time string.\n  ///\n  /// Returns a string in the format \"MM/dd HH:mm:ss\" (e.g., \"12/25 14:30:45\").\n  String toFullSimpleDateTimeString() => asTZDateTime.toFullSimpleDateTimeString();\n\n  /// Formats this timestamp as a locale-aware full date string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd (EEEE)\" (e.g., \"2024/12/25 (Wednesday)\"). The day of the week is\n  /// localized according to [context]'s locale.\n  String toLocaleFullDateString(BuildContext context) =>\n      asTZDateTime.toLocaleFullDateString(context);\n\n  /// Formats this timestamp as a locale-aware date-time string.\n  ///\n  /// Returns a string in the format \"yyyy/MM/dd HH:mm:ss\" (e.g., \"2024/12/25 14:30:45\"). The format is localized\n  /// according to [context]'s locale.\n  String toLocaleDateTimeString(BuildContext context) =>\n      asTZDateTime.toLocaleDateTimeString(context);\n\n  /// Formats this timestamp as a locale-aware time string.\n  ///\n  /// Returns a string in the format \"HH:mm:ss\" (e.g., \"14:30:45\"). The format is localized according to [context]'s\n  /// locale.\n  String toLocaleTimeString(BuildContext context) => asTZDateTime.toLocaleTimeString(context);\n}\n\nRegExp _trailingRegex = RegExp(r'([.]*0)(?!.*\\d)');\n\n/// Extension on [double] that provides convenient utilities for working with double values.\n///\n/// This extension adds helpful methods and getters to simplify common operations on [double] values, such as unit\n/// conversions and formatting.\nextension DoubleExtension on double {\n  /// Round this to a specific floating precision\n  double precision(int precision) {\n    final mod = pow(10.0, precision);\n    return ((this * mod).round() / mod);\n  }\n\n  /// Round this to a specific floating precision and convert it to a [String] without trailing zeros\n  String precisionString(int precision) {\n    final mod = pow(10.0, precision);\n    final value = ((this * mod).round() / mod).toString();\n\n    return value.replaceAll(_trailingRegex, '');\n  }\n}\n\n/// Extension for number conversions\nextension NumberConvert on num {\n  /// Converts this to a double.\n  double get asDouble => this.toDouble();\n\n  /// Converts this to a integer.\n  int get asInt => this.toInt();\n\n  /// Converts this to a percentage.\n  int get asPercentage => (this * 100).truncate();\n\n  /// Converts this to a TZDateTime in Asia/Taipei Timezone.\n  ///\n  /// This calls [TZDateTime.fromMillisecondsSinceEpoch] under the hood.\n  TZDateTime get asTZDateTime =>\n      .fromMillisecondsSinceEpoch(getLocation('Asia/Taipei'), this.asInt);\n\n  /// Converts this temperature value from Celsius to Fahrenheit.\n  ///\n  /// The conversion formula is: `F = C × 9/5 + 32`. The result preserves decimal precision, returning a [double] value.\n  ///\n  /// Example:\n  /// ```dart\n  /// final celsius = 25.5;\n  /// final fahrenheit = celsius.asFahrenheit; // 77.9\n  ///\n  /// final zeroCelsius = 0.0;\n  /// final zeroFahrenheit = zeroCelsius.asFahrenheit; // 32.0\n  /// ```\n  double get asFahrenheit => this * 9 / 5 + 32;\n}\n"
  },
  {
    "path": "lib/utils/extensions/preference.dart",
    "content": "import 'package:dpip/utils/log.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\n/// Extension on [SharedPreferencesWithCache] that provides convenient utilities for preference storage.\n///\n/// This extension adds helpful methods to simplify storing and managing preferences, including type-safe value storage\n/// with automatic type handling and logging.\nextension PreferenceExtension on SharedPreferencesWithCache {\n  /// Sets a value of any supported type to SharedPreferences.\n  ///\n  /// This is a convenience method that automatically handles different types supported by SharedPreferences. It calls\n  /// the appropriate type-specific setter method based on the runtime type of [value].\n  ///\n  /// Supported types are: [String], [int], [bool], [double], and [List<String>]. If [value] is `null` or omitted, the\n  /// key will be removed from SharedPreferences.\n  ///\n  /// All operations are logged for debugging purposes. If an error occurs, it is logged and rethrown.\n  ///\n  /// Throws [ArgumentError] if [value] is of an unsupported type.\n  ///\n  /// Example:\n  /// ```dart\n  /// // Store a string value\n  /// await preferences.set('username', 'John');\n  ///\n  /// // Store an integer value\n  /// await preferences.set('age', 25);\n  ///\n  /// // Store a boolean value\n  /// await preferences.set('isEnabled', true);\n  ///\n  /// // Remove a key by passing null\n  /// await preferences.set('username', null);\n  /// ```\n  Future<void> set<T>(String key, [T? value]) {\n    try {\n      if (value == null) {\n        return remove(key);\n      }\n\n      switch (value) {\n        case String():\n          return setString(key, value);\n        case int():\n          return setInt(key, value);\n        case bool():\n          return setBool(key, value);\n        case double():\n          return setDouble(key, value);\n        case List<String>():\n          return setStringList(key, value);\n        default:\n          throw ArgumentError.value(\n            value,\n            'value',\n            'Unsupported type: ${value.runtimeType}',\n          );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('💾 $key set to \"$value\" FAILED', e, s);\n      rethrow;\n    } finally {\n      TalkerManager.instance.info('💾 $key set to \"$value\"');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/extensions/product_detail.dart",
    "content": "import 'package:in_app_purchase/in_app_purchase.dart';\n\n/// Extension on [ProductDetails] that provides convenient utilities for product type identification.\n///\n/// This extension adds helpful getters to simplify identifying product types, particularly distinguishing between\n/// subscription and one-time purchase products.\nextension ProductDetailExtension on ProductDetails {\n  /// Checks whether this product is a subscription.\n  ///\n  /// Returns `true` if the product ID starts with 's_', indicating it is a subscription product. Returns `false` for\n  /// one-time purchase products.\n  ///\n  /// This is useful for filtering and displaying products differently based on their type, as subscriptions and\n  /// one-time purchases often require different UI and purchase flows.\n  ///\n  /// Example:\n  /// ```dart\n  /// final products = await InAppPurchase.instance.queryProductDetails(productIds);\n  /// final subscriptions = products.productDetails.where((p) => p.isSubscription).toList();\n  /// final oneTimePurchases = products.productDetails.where((p) => !p.isSubscription).toList();\n  /// ```\n  bool get isSubscription => id.startsWith('s_');\n}\n"
  },
  {
    "path": "lib/utils/extensions/string.dart",
    "content": "import 'package:dpip/api/model/location/location.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/datetime.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// Extension on [String] that provides convenient utilities for type conversion and formatting.\n///\n/// This extension adds helpful methods and getters to simplify common string operations, including type conversions,\n/// date-time formatting, widget creation, and location parsing.\nextension StringExtension on String {\n  /// Converts this string to a [Locale] object.\n  ///\n  /// The string should be in the format \"language-country\" (e.g., \"en-US\", \"zh-TW\"). The method splits the string by\n  /// '-' and creates a [Locale] with the language code as the first part and the country code as the optional second\n  /// part.\n  Locale get asLocale {\n    final a = split('-');\n    return Locale(a[0], a.elementAtOrNull(1));\n  }\n\n  /// Converts this string to a [Uri] object.\n  ///\n  /// Parses this string as a URI. Throws a [FormatException] if the string is not a valid URI.\n  Uri get asUri => .parse(this);\n\n  /// Converts this string to an integer.\n  ///\n  /// Parses this string as an integer. Throws a [FormatException] if the string is not a valid integer.\n  int get asInt => .parse(this);\n\n  /// Converts this string to a double.\n  ///\n  /// Parses this string as a double. Throws a [FormatException] if the string is not a valid double.\n  double get asDouble => .parse(this);\n\n  /// Formats this string as a timestamp and converts it to a locale-aware full date string.\n  ///\n  /// This method first converts the string to an integer (milliseconds since epoch), then formats it as a locale-aware\n  /// full date string in the format \"yyyy/MM/dd (EEEE)\" (e.g., \"2024/12/25 (Wednesday)\"). The day of the week is\n  /// localized according to [context]'s locale.\n  String toLocaleFullDateString(BuildContext context) =>\n      asInt.asTZDateTime.toLocaleFullDateString(context);\n\n  /// Formats this string as a timestamp and converts it to a locale-aware time string.\n  ///\n  /// This method first converts the string to an integer (milliseconds since epoch), then formats it as a locale-aware\n  /// time string in the format \"HH:mm:ss\" (e.g., \"14:30:45\"). The format is localized according to [context]'s locale.\n  String toLocaleTimeString(BuildContext context) => asInt.asTZDateTime.toLocaleTimeString(context);\n\n  /// Formats this string as a timestamp and converts it to a simple date-time string.\n  ///\n  /// This method first converts the string to an integer (milliseconds since epoch), then formats it as a simple\n  /// date-time string in the format \"MM/dd HH:mm\" (e.g., \"12/25 14:30\").\n  String toSimpleDateTimeString() => asInt.asTZDateTime.toSimpleDateTimeString();\n\n  /// Converts this string to a [Text] widget.\n  ///\n  /// Creates a [Text] widget with this string as its content. This is useful for quickly creating text widgets from\n  /// strings in widget trees.\n  ///\n  /// Example:\n  /// ```dart\n  /// final widget = 'Hello'.asText;\n  /// ```\n  Text get asText => .new(this);\n\n  /// Converts this string to a [TextSpan] object.\n  ///\n  /// Creates a [TextSpan] with this string as its text content. This is useful for building rich text widgets with\n  /// multiple spans.\n  ///\n  /// Example:\n  /// ```dart\n  /// final span = 'Hello'.asTextSpan;\n  /// ```\n  TextSpan get asTextSpan => .new(text: this);\n\n  /// Gets the [Location] object associated with this location code.\n  ///\n  /// Looks up this string as a location code in the global location map and returns the corresponding [Location].\n  /// Throws an exception if the location code is not found.\n  ///\n  /// Example:\n  /// ```dart\n  /// final location = '10001'.getLocation();\n  /// ```\n  Location getLocation() => Global.location[this]!;\n\n  /// Attempts to parse this string as a [Location] object.\n  ///\n  /// Parses this string as a location code and returns the corresponding [Location] if successful, or `null` if the\n  /// string cannot be parsed or the location is not found.\n  ///\n  /// Example:\n  /// ```dart\n  /// final location = '10001'.asLocation; // Location or null\n  /// ```\n  Location? get asLocation => .tryParse(this);\n\n  /// Launches this string as a URL in the default external application.\n  ///\n  /// Converts this string to a [Uri] and opens it using the platform's default handler. The URL will typically open in\n  /// the default browser, but may open in other applications depending on the URL scheme (e.g., `mailto:` opens in the\n  /// mail client, `tel:` opens in the phone dialer).\n  ///\n  /// Throws a [FormatException] if this string is not a valid URI. May also throw platform-specific exceptions if the\n  /// URL cannot be launched.\n  ///\n  /// Example:\n  /// ```dart\n  /// 'https://example.com'.launch(); // Opens in browser\n  /// 'mailto:user@example.com'.launch(); // Opens mail client\n  /// ```\n  void launch() => launchUrl(asUri);\n\n  /// Copies this string to the system clipboard.\n  ///\n  /// Sets this string as the clipboard data, making it available for pasting in other applications. This is a\n  /// convenience method that wraps [Clipboard.setData] with a [ClipboardData] object containing this string as text.\n  ///\n  /// Example:\n  /// ```dart\n  /// 'Hello, World!'.copy(); // Copies text to clipboard\n  /// userEmail.copy(); // Copies email to clipboard\n  /// ```\n  Future<void> copy() => Clipboard.setData(\n    ClipboardData(text: this),\n  ).catchError((e) => TalkerManager.instance.error(e));\n}\n"
  },
  {
    "path": "lib/utils/extensions/theme.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension ThemeModeExtension on ThemeMode {\n  String get label => switch (this) {\n    .light => '淺色',\n    .dark => '深色',\n    .system => '跟隨系統',\n  };\n}\n"
  },
  {
    "path": "lib/utils/functions.dart",
    "content": "/// A no-operation function that does nothing.\n///\n/// This function is useful as a placeholder callback or default handler when a function parameter\n/// is required but no action is needed. It can be used to satisfy function type requirements without\n/// performing any operations.\n///\n/// Example:\n/// ```dart\n/// // Use as a default callback\n/// onTap: someCondition ? handleTap : noop;\n///\n/// // Use as a placeholder\n/// final callback = noop;\n/// ```\nvoid noop() {}\n\n/// Comparison function for ascending order sorting.\n///\n/// Used with [List.sort] or [Iterable.sorted] to sort numeric values in ascending order (smallest to largest).\n///\n/// Example:\n/// ```dart\n/// final numbers = [3, 1, 4, 1, 5];\n/// numbers.sort(ascending); // [1, 1, 3, 4, 5]\n/// ```\nint ascending(num a, num b) => a.compareTo(b);\n\n/// Comparison function for descending order sorting.\n///\n/// Used with [List.sort] or [Iterable.sorted] to sort numeric values in descending order (largest to smallest).\n///\n/// Example:\n/// ```dart\n/// final numbers = [3, 1, 4, 1, 5];\n/// numbers.sort(descending); // [5, 4, 3, 1, 1]\n/// ```\nint descending(num a, num b) => b.compareTo(a);\n"
  },
  {
    "path": "lib/utils/geojson.dart",
    "content": "/// GeoJSON geometry types supported by the builder classes.\n///\n/// These types correspond to the geometry types defined in the GeoJSON specification:\n/// - [Point]: A single coordinate point\n/// - [Polygon]: A closed area defined by one or more linear rings\n/// - [LineString]: A sequence of connected points forming a line\nenum GeoJsonFeatureType {\n  /// A single coordinate point.\n  ///\n  /// Represents a single geographic location defined by a coordinate pair `[longitude, latitude]`.\n  /// This is the simplest geometry type in GeoJSON, typically used for markers, pins, or point\n  /// locations on a map.\n  ///\n  /// When using this type with [GeoJsonFeatureBuilder], the coordinates should be a single\n  /// coordinate pair: `[longitude, latitude]`.\n  Point,\n\n  /// A closed area defined by one or more linear rings.\n  ///\n  /// Represents a polygon geometry, which is a closed area bounded by linear rings. The first\n  /// linear ring defines the exterior boundary, and any additional rings define holes within the\n  /// polygon.\n  ///\n  /// When using this type with [GeoJsonFeatureBuilder], the coordinates should be an array of\n  /// linear rings, where each linear ring is an array of coordinate pairs. The first ring is the\n  /// exterior boundary, and subsequent rings are holes.\n  Polygon,\n\n  /// A sequence of connected points forming a line.\n  ///\n  /// Represents a line geometry, which is a sequence of connected points forming a continuous\n  /// path. This is typically used for roads, paths, boundaries, or any linear features on a map.\n  ///\n  /// When using this type with [GeoJsonFeatureBuilder], the coordinates should be an array of\n  /// coordinate pairs: `[[lon1, lat1], [lon2, lat2], ...]`.\n  LineString,\n}\n\n/// Builder class for constructing GeoJSON FeatureCollection objects.\n///\n/// This class provides a fluent interface for building GeoJSON FeatureCollections, which are collections\n/// of GeoJSON features. All methods return the builder instance to support method chaining.\n///\n/// Example:\n/// ```dart\n/// final builder = GeoJsonBuilder()\n///   ..addFeature(pointFeature)\n///   ..addFeature(polygonFeature);\n/// final geoJson = builder.build();\n/// ```\nclass GeoJsonBuilder {\n  /// The list of features in this FeatureCollection.\n  List<GeoJsonFeatureBuilder> features = [];\n\n  /// Creates a new [GeoJsonBuilder] instance.\n  GeoJsonBuilder();\n\n  /// Returns an empty GeoJSON FeatureCollection.\n  ///\n  /// This is a convenience getter that returns a valid but empty FeatureCollection map.\n  static Map<String, dynamic> get empty => GeoJsonBuilder().build();\n\n  /// Sets all features at once, replacing any existing features.\n  ///\n  /// The [features] parameter is converted to a list. This method clears any previously added features\n  /// and replaces them with the provided features.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonBuilder setFeatures(Iterable<GeoJsonFeatureBuilder> features) {\n    this.features = features.toList();\n    return this;\n  }\n\n  /// Adds a single feature to this FeatureCollection.\n  ///\n  /// The [feature] is appended to the list of features. Multiple features can be added by calling\n  /// this method multiple times.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonBuilder addFeature(GeoJsonFeatureBuilder feature) {\n    features.add(feature);\n    return this;\n  }\n\n  /// Removes all features from this builder.\n  ///\n  /// Clears the features list, effectively resetting the builder to an empty state.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonBuilder clearFeatures() {\n    features = [];\n    return this;\n  }\n\n  /// Builds and returns the GeoJSON FeatureCollection map.\n  ///\n  /// Returns a map representing a complete GeoJSON FeatureCollection, ready to be serialized to JSON.\n  /// The map contains a 'type' field set to 'FeatureCollection' and a 'features' array containing\n  /// all added features.\n  Map<String, dynamic> build() {\n    return {\n      'type': 'FeatureCollection',\n      'features': features.map((f) => f.build()).toList(),\n    };\n  }\n}\n\n/// Builder class for constructing GeoJSON Feature objects.\n///\n/// This class provides a fluent interface for building individual GeoJSON features, which consist of\n/// a geometry type, coordinates, optional ID, and properties. All methods return the builder instance\n/// to support method chaining.\n///\n/// The geometry type is specified at construction time and determines how coordinates are interpreted:\n/// - [GeoJsonFeatureType.Point]: Coordinates should be a single coordinate pair `[longitude, latitude]`\n/// - [GeoJsonFeatureType.Polygon]: Coordinates should be an array of linear rings (arrays of coordinate pairs)\n/// - [GeoJsonFeatureType.LineString]: Coordinates should be an array of coordinate pairs\n///\n/// Example:\n/// ```dart\n/// final feature = GeoJsonFeatureBuilder(GeoJsonFeatureType.Point)\n///   ..setGeometry([121.5, 25.0])\n///   ..setProperty('name', 'Taipei')\n///   ..setId(1);\n/// final geoJson = feature.build();\n/// ```\nclass GeoJsonFeatureBuilder<T extends GeoJsonFeatureType> {\n  /// The geometry type of this feature.\n  T type;\n\n  /// Optional feature ID.\n  int? id;\n\n  /// The coordinates for this feature's geometry.\n  ///\n  /// The format depends on the [type]:\n  /// - For [GeoJsonFeatureType.Point]: `[longitude, latitude]`\n  /// - For [GeoJsonFeatureType.LineString]: `[[lon1, lat1], [lon2, lat2], ...]`\n  /// - For [GeoJsonFeatureType.Polygon]: `[[[lon1, lat1], [lon2, lat2], ...]]` (array of linear rings)\n  List<dynamic> coordinates = [];\n\n  /// Custom properties associated with this feature.\n  ///\n  /// Properties can contain any additional metadata about the feature, such as names, values, or\n  /// other attributes.\n  Map<String, dynamic> properties = {};\n\n  /// Creates a new [GeoJsonFeatureBuilder] with the specified geometry [type].\n  GeoJsonFeatureBuilder(this.type);\n\n  /// Sets the optional feature ID.\n  ///\n  /// The [id] is included in the built feature if provided. Feature IDs are useful for identifying\n  /// features in GeoJSON data.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonFeatureBuilder setId(int id) {\n    this.id = id;\n    return this;\n  }\n\n  /// Sets the geometry coordinates for this feature.\n  ///\n  /// The format of [coordinates] depends on the feature [type]:\n  /// - For [GeoJsonFeatureType.Point]: A single coordinate pair `[longitude, latitude]`\n  /// - For [GeoJsonFeatureType.LineString]: An array of coordinate pairs\n  /// - For [GeoJsonFeatureType.Polygon]: An array of linear rings (arrays of coordinate pairs)\n  ///\n  /// The method automatically handles coordinate wrapping for Polygon and LineString types. If all\n  /// elements in [coordinates] are lists, it wraps them in an additional array level for Polygon types.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonFeatureBuilder setGeometry(List<dynamic> coordinates) {\n    if (type == GeoJsonFeatureType.Point) {\n      this.coordinates = coordinates;\n      return this;\n    }\n\n    if (coordinates.every((element) => element is List)) {\n      this.coordinates = [coordinates];\n    } else {\n      this.coordinates = coordinates;\n    }\n    return this;\n  }\n\n  /// Sets a property value for this feature.\n  ///\n  /// Properties are key-value pairs that provide additional metadata about the feature. Multiple\n  /// properties can be set by calling this method multiple times.\n  ///\n  /// Returns this builder for method chaining.\n  GeoJsonFeatureBuilder setProperty(String key, dynamic value) {\n    properties[key] = value;\n    return this;\n  }\n\n  /// Builds and returns the GeoJSON Feature map.\n  ///\n  /// Returns a map representing a complete GeoJSON Feature, ready to be serialized to JSON.\n  /// The map contains 'type', 'geometry', and 'properties' fields. The 'id' field is included only\n  /// if it was set via [setId].\n  Map<String, dynamic> build() {\n    final result = <String, dynamic>{\n      'type': 'Feature',\n      'properties': properties,\n      'geometry': {'type': type.name, 'coordinates': coordinates},\n    };\n    if (id != null) {\n      result['id'] = id;\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/utils/instrumental_intensity_color.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass InstrumentalIntensityColor {\n  InstrumentalIntensityColor._();\n\n  static const intensity_3 = Color(0xff0005d0);\n  static const intensity_2 = Color(0xff004bf8);\n  static const intensity_1 = Color(0xff009EF8);\n  static const intensity0 = Color(0xff79E5FD);\n  static const intensity1 = Color(0xff49E9AD);\n  static const intensity2 = Color(0xff44fa34);\n  static const intensity3 = Color(0xffbeff0c);\n  static const intensity4 = Color(0xfffff000);\n  static const intensity5 = Color(0xffff9300);\n  static const intensity6 = Color(0xfffc5235);\n  static const intensity7 = Color(0xffb720e9);\n\n  static Color intensity(int intensity) {\n    switch (intensity) {\n      case -3:\n        return intensity_3;\n      case -2:\n        return intensity_2;\n      case -1:\n        return intensity_1;\n      case 0:\n        return intensity0;\n      case 1:\n        return intensity1;\n      case 2:\n        return intensity2;\n      case 3:\n        return intensity3;\n      case 4:\n        return intensity4;\n      case 5:\n        return intensity5;\n      case 6:\n        return intensity6;\n      case 7:\n        return intensity7;\n      default:\n        throw 'Intensity index out of range. Range: 0..9, Received: $intensity';\n    }\n  }\n\n  static Color i(double? intensity) {\n    if (intensity == null) {\n      return Colors.transparent;\n    }\n\n    final ceil = intensity.ceil();\n    final ceilColor = InstrumentalIntensityColor.intensity(ceil);\n    final floor = intensity.floor();\n    final floorColor = InstrumentalIntensityColor.intensity(floor);\n    final tween = ColorTween(begin: floorColor, end: ceilColor);\n    return tween.lerp(intensity - floor)!;\n  }\n}\n"
  },
  {
    "path": "lib/utils/intensity_color.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass IntensityColor {\n  IntensityColor._();\n\n  static const intensity0 = Colors.grey;\n  static const intensity1 = Color(0xff003264);\n  static const intensity2 = Color(0xff0064c8);\n  static const intensity3 = Color(0xff1e9632);\n  static const intensity4 = Color(0xffffc800);\n  static const intensity5 = Color(0xffff9600);\n  static const intensity6 = Color(0xffff6400);\n  static const intensity7 = Color(0xffff0000);\n  static const intensity8 = Color(0xffc00000);\n  static const intensity9 = Color(0xff9600c8);\n\n  static Color intensity(int intensity) {\n    switch (intensity) {\n      case 0:\n        return IntensityColor.intensity0;\n      case 1:\n        return IntensityColor.intensity1;\n      case 2:\n        return IntensityColor.intensity2;\n      case 3:\n        return IntensityColor.intensity3;\n      case 4:\n        return IntensityColor.intensity4;\n      case 5:\n        return IntensityColor.intensity5;\n      case 6:\n        return IntensityColor.intensity6;\n      case 7:\n        return IntensityColor.intensity7;\n      case 8:\n        return IntensityColor.intensity8;\n      case 9:\n        return IntensityColor.intensity9;\n      default:\n        throw 'Intensity index out of range. Range: 0..9, Received: $intensity';\n    }\n  }\n\n  static Color onIntensity(int intensity) {\n    switch (intensity) {\n      case 4:\n      case 5:\n        return Colors.black;\n      default:\n        return Colors.white;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/intervals.dart",
    "content": "enum Intervals {\n  now,\n  tenMinutes,\n  oneHour,\n  threeHours,\n  sixHours,\n  twelveHours,\n  twentyFourHours,\n  twoDays,\n  threeDays,\n}\n"
  },
  {
    "path": "lib/utils/list_icon.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\nIconData getListIcon(String name) => switch (name) {\n  'bolt_rounded' => Symbols.bolt_rounded,\n  'landslide_rounded' => Symbols.landslide_rounded,\n  'earthquake_rounded' => Symbols.earthquake_rounded,\n  'rainy_rounded' => Symbols.rainy_rounded,\n  'flood_rounded' => Symbols.flood_rounded,\n  'tsunami_rounded' => Symbols.tsunami_rounded,\n  'volcano_rounded' => Symbols.volcano_rounded,\n  'thermometer_add_rounded' => Symbols.thermometer_add_rounded,\n  'thermometer_minus_rounded' => Symbols.thermometer_minus_rounded,\n  'air_rounded' => Symbols.air_rounded,\n  'emergency_heat_rounded' => Symbols.emergency_heat_rounded,\n  'medical_mask_rounded' => Symbols.medical_mask_rounded,\n  'bomb_rounded' => Symbols.bomb_rounded,\n  'warning_rounded' => Symbols.warning_rounded,\n  'directions_run_rounded' => Symbols.directions_run_rounded,\n  'destruction_rounded' => Symbols.destruction_rounded,\n  'power_off_rounded' => Symbols.power_off_rounded,\n  'format_color_reset_rounded' => Symbols.format_color_reset_rounded,\n  'water_rounded' => Symbols.water_rounded,\n  'cyclone_rounded' => Symbols.cyclone_rounded,\n  'foggy' => Symbols.foggy,\n  _ => Symbols.error,\n};\n"
  },
  {
    "path": "lib/utils/log.dart",
    "content": "import 'package:talker_flutter/talker_flutter.dart';\n\nclass CustomLoggerFormatter implements LoggerFormatter {\n  const CustomLoggerFormatter();\n\n  @override\n  String fmt(LogDetails details, TalkerLoggerSettings settings) {\n    final msg = details.message?.toString() ?? '';\n    if (!settings.enableColors) {\n      return msg;\n    }\n    return msg.split('\\n').map((e) => details.pen.write(e)).join('\\n');\n  }\n}\n\nclass TalkerManager {\n  TalkerManager._();\n\n  static final Talker _instance = Talker(\n    logger: TalkerLogger(formatter: const CustomLoggerFormatter()),\n  );\n\n  static Talker get instance => _instance;\n}\n"
  },
  {
    "path": "lib/utils/magnitude_color.dart",
    "content": "import 'dart:ui';\n\nclass MagnitudeColor {\n  MagnitudeColor._();\n\n  static const magnitude0 = Color(0xFF000000);\n  static const magnitude1 = Color(0xFF00C8C8);\n  static const magnitude2 = Color(0xFF00C800);\n  static const magnitude3 = Color(0xFFFFC800);\n  static const magnitude4 = Color(0xFFFF0000);\n  static const magnitude5 = Color(0xFF9600FF);\n\n  static Color magnitude(double mag) {\n    final magnitudeList = [2.5, 3.5, 4.5, 6.0, 7.0];\n    final colorList = [\n      magnitude1,\n      magnitude2,\n      magnitude3,\n      magnitude4,\n      magnitude5,\n    ];\n\n    if (mag <= magnitudeList.first) {\n      return colorList.first;\n    }\n\n    if (mag >= magnitudeList.last) {\n      return colorList.last;\n    }\n\n    for (int i = 0; i < magnitudeList.length - 1; i++) {\n      if (mag >= magnitudeList[i] && mag < magnitudeList[i + 1]) {\n        final double localT = (mag - magnitudeList[i]) / (magnitudeList[i + 1] - magnitudeList[i]);\n        return Color.lerp(colorList[i], colorList[i + 1], localT)!;\n      }\n    }\n\n    return magnitude0;\n  }\n}\n"
  },
  {
    "path": "lib/utils/map_utils.dart",
    "content": "import 'dart:math';\n\nimport 'package:dpip/api/model/eew.dart';\nimport 'package:dpip/global.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:geojson_vi/geojson_vi.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nenum Units {\n  meters,\n  metres,\n  millimeters,\n  millimetres,\n  centimeters,\n  centimetres,\n  kilometers,\n  kilometres,\n  miles,\n  nauticalmiles,\n  inches,\n  yards,\n  feet,\n  radians,\n  degrees,\n}\n\n/// Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth.\n///\n/// @memberof helpers\n/// @type {number}\nconst earthRadius = 6371008.8;\n\n/// Unit of measurement factors using a spherical (non-ellipsoid) earth radius.\n///\n/// Keys are the name of the unit, values are the number of that unit in a single radian\nconst Map<Units, double> factors = {\n  Units.centimeters: earthRadius * 100,\n  Units.centimetres: earthRadius * 100,\n  Units.degrees: 360 / (2 * pi),\n  Units.feet: earthRadius * 3.28084,\n  Units.inches: earthRadius * 39.37,\n  Units.kilometers: earthRadius / 1000,\n  Units.kilometres: earthRadius / 1000,\n  Units.meters: earthRadius,\n  Units.metres: earthRadius,\n  Units.miles: earthRadius / 1609.344,\n  Units.millimeters: earthRadius * 1000,\n  Units.millimetres: earthRadius * 1000,\n  Units.nauticalmiles: earthRadius / 1852,\n  Units.radians: 1,\n  Units.yards: earthRadius * 1.0936,\n};\n\n// Math constants for degree/radian conversion\nconst _degToRad = pi / 180;\nconst _radToDeg = 180 / pi;\nconst _twoPi = 2 * pi;\n\n/// Converts an angle in degrees to radians\ndouble degreesToRadians(double degrees) {\n  final radians = degrees % 360;\n  return radians * _degToRad;\n}\n\n/// Converts an angle in radians to degrees\ndouble radiansToDegrees(double radians) {\n  final degrees = radians % _twoPi;\n  return degrees * _radToDeg;\n}\n\n// Precomputed factor for kilometers (most common unit for EEW)\nconst _kmToRadiansFactor = 1000 / earthRadius;\n\n/// Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians\n/// Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\ndouble lengthToRadians(double distance, {Units units = Units.kilometers}) {\n  // Fast path for kilometers (most common for EEW)\n  if (units == Units.kilometers) {\n    return distance * _kmToRadiansFactor;\n  }\n\n  final factor = factors[units];\n  if (factor == null) {\n    throw '$units units is invalid';\n  }\n  return distance / factor;\n}\n\n/// Takes a [LatLng] and calculates the location of a destination point given a distance in\n/// degrees, radians, miles, or kilometers; and bearing in degrees.\n/// This uses the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature.\nLatLng destination(\n  LatLng origin,\n  double distance,\n  double bearing, {\n  Units units = Units.kilometers,\n}) {\n  // Handle input\n  final longitude1 = degreesToRadians(origin.longitude);\n  final latitude1 = degreesToRadians(origin.latitude);\n  final bearingRad = degreesToRadians(bearing);\n  final radians = lengthToRadians(distance, units: units);\n\n  // Main\n  final latitude2 = asin(\n    sin(latitude1) * cos(radians) + cos(latitude1) * sin(radians) * cos(bearingRad),\n  );\n  final longitude2 =\n      longitude1 +\n      atan2(\n        sin(bearingRad) * sin(radians) * cos(latitude1),\n        cos(radians) - sin(latitude1) * sin(latitude2),\n      );\n  final lng = radiansToDegrees(longitude2);\n  final lat = radiansToDegrees(latitude2);\n\n  return LatLng(lat, lng);\n}\n\n/// Takes a [LatLng] and calculates the circle polygon given a radius in\n/// degrees, radians, miles, or kilometers; and steps for precision.\n@Deprecated('Use circleFeature()')\nMap<String, dynamic> circle(\n  LatLng center,\n  double radius, {\n  int steps = 64,\n  Units units = Units.kilometers,\n}) {\n  // main\n  final coordinates = [];\n\n  for (var i = 0; i < steps; i++) {\n    final point = destination(center, radius, (i * -360) / steps, units: units);\n    coordinates.add(point.asGeoJsonCooridnate);\n  }\n\n  coordinates.add(coordinates[0]);\n\n  return {\n    'type': 'Feature',\n    'properties': {},\n    'geometry': {\n      'coordinates': [coordinates],\n      'type': 'Polygon',\n    },\n  };\n}\n\n// Precomputed bearing values for circle generation (32 steps)\nfinal _circleBearings32 = List.generate(\n  32,\n  (i) => degreesToRadians((i * -360) / 32),\n);\nfinal _circleSines32 = _circleBearings32.map((b) => sin(b)).toList();\nfinal _circleCosines32 = _circleBearings32.map((b) => cos(b)).toList();\n\n/// Takes a [LatLng] and calculates the circle polygon given a radius in\n/// degrees, radians, miles, or kilometers; and steps for precision.\n///\n/// Optimized version that precomputes trigonometric values for 32 steps.\nGeoJsonFeatureBuilder circleFeature({\n  required LatLng center,\n  required double radius,\n  int steps = 32,\n  Units units = Units.kilometers,\n}) {\n  final polygon = GeoJsonFeatureBuilder(GeoJsonFeatureType.Polygon);\n  final List<List<double>> coordinates = [];\n\n  if (steps == 32) {\n    // Fast path: use precomputed values\n    final longitude1 = degreesToRadians(center.longitude);\n    final latitude1 = degreesToRadians(center.latitude);\n    final radians = lengthToRadians(radius, units: units);\n\n    final sinLat1 = sin(latitude1);\n    final cosLat1 = cos(latitude1);\n    final sinRadians = sin(radians);\n    final cosRadians = cos(radians);\n\n    // Precompute loop-invariant values\n    final sinLat1CosRadians = sinLat1 * cosRadians;\n    final cosLat1SinRadians = cosLat1 * sinRadians;\n\n    for (var i = 0; i < 32; i++) {\n      final sinBearing = _circleSines32[i];\n      final cosBearing = _circleCosines32[i];\n\n      final latitude2 = asin(\n        sinLat1CosRadians + cosLat1SinRadians * cosBearing,\n      );\n      final longitude2 =\n          longitude1 +\n          atan2(\n            sinBearing * cosLat1SinRadians,\n            cosRadians - sinLat1 * sin(latitude2),\n          );\n\n      coordinates.add([\n        radiansToDegrees(longitude2),\n        radiansToDegrees(latitude2),\n      ]);\n    }\n  } else {\n    // Fallback: original implementation\n    for (var i = 0; i < steps; i++) {\n      final point = destination(\n        center,\n        radius,\n        (i * -360) / steps,\n        units: units,\n      );\n      coordinates.add(point.asGeoJsonCooridnate);\n    }\n  }\n\n  coordinates.add(coordinates[0]);\n\n  return polygon.setGeometry(coordinates);\n}\n\nbool checkBoxSkip(\n  Map<String, Eew> eewLastInfo,\n  Map<String, double> eewDist,\n  List<List<double>> box,\n) {\n  bool passed = false;\n\n  for (final eew in eewLastInfo.keys) {\n    int skip = 0;\n    for (int i = 0; i < 4; i++) {\n      final dist = LatLng(\n        eewLastInfo[eew]!.info.latitude,\n        eewLastInfo[eew]!.info.longitude,\n      ).to(LatLng(box[i][1], box[i][0]));\n\n      if (eewDist[eew]! > dist) skip++;\n    }\n    if (skip >= 4) {\n      passed = true;\n      break;\n    }\n  }\n\n  return passed;\n}\n\nString? getTownCodeFromCoordinates(LatLng target) {\n  final features = Global.townGeojson.features;\n\n  for (final feature in features) {\n    if (feature == null) continue;\n\n    final geometry = feature.geometry;\n    if (geometry == null) continue;\n\n    bool isInPolygon = false;\n\n    if (geometry is GeoJSONPolygon) {\n      final polygon = geometry.coordinates[0];\n\n      bool isInside = false;\n      int j = polygon.length - 1;\n      for (int i = 0; i < polygon.length; i++) {\n        final double xi = polygon[i][0];\n        final double yi = polygon[i][1];\n        final double xj = polygon[j][0];\n        final double yj = polygon[j][1];\n\n        final bool intersect =\n            ((yi > target.latitude) != (yj > target.latitude)) &&\n            (target.longitude < (xj - xi) * (target.latitude - yi) / (yj - yi) + xi);\n        if (intersect) isInside = !isInside;\n\n        j = i;\n      }\n      isInPolygon = isInside;\n    }\n\n    if (geometry is GeoJSONMultiPolygon) {\n      final multiPolygon = geometry.coordinates;\n\n      for (final polygonCoordinates in multiPolygon) {\n        final polygon = polygonCoordinates[0];\n\n        bool isInside = false;\n        int j = polygon.length - 1;\n        for (int i = 0; i < polygon.length; i++) {\n          final double xi = polygon[i][0];\n          final double yi = polygon[i][1];\n          final double xj = polygon[j][0];\n          final double yj = polygon[j][1];\n\n          final bool intersect =\n              ((yi > target.latitude) != (yj > target.latitude)) &&\n              (target.longitude < (xj - xi) * (target.latitude - yi) / (yj - yi) + xi);\n          if (intersect) isInside = !isInside;\n\n          j = i;\n        }\n\n        if (isInside) {\n          isInPolygon = true;\n          break;\n        }\n      }\n    }\n\n    if (isInPolygon) {\n      return feature.properties!['CODE']?.toString();\n    }\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "lib/utils/need_location.dart",
    "content": "import 'package:dpip/router.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nFuture<void> showLocationDialog(BuildContext context) async {\n  await showDialog(\n    context: context,\n    barrierDismissible: false,\n    builder: (context) {\n      return AlertDialog(\n        icon: const Icon(Symbols.error),\n        title: const Text('尚未設定所在地'),\n        content: const Text('DPIP 需要設定所在地才能正常運作。點擊「前往設定」設定所在地後再試一次。'),\n        actionsAlignment: MainAxisAlignment.end,\n        actions: [\n          TextButton(\n            child: const Text('前往設定'),\n            onPressed: () {\n              Navigator.pop(context);\n              SettingsIndexRoute().push(context);\n              SettingsLocationRoute().push(context);\n            },\n          ),\n        ],\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/utils/page_route_builder/forward_back.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ForwardBackPageRouteBuilder extends PageRouteBuilder {\n  final Widget page;\n\n  ForwardBackPageRouteBuilder({required this.page})\n    : super(pageBuilder: (context, animation, secondaryAnimation) => page);\n\n  final backTransition = const Interval(\n    0,\n    0.5,\n    curve: Easing.emphasizedAccelerate,\n  );\n  final forwardTransition = const Interval(\n    0.5,\n    1,\n    curve: Easing.emphasizedDecelerate,\n  );\n\n  @override\n  Duration get transitionDuration => Durations.long4;\n\n  @override\n  Duration get reverseTransitionDuration => Durations.long4;\n\n  @override\n  SlideTransition Function(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n    Widget child,\n  )\n  get transitionsBuilder => (context, animation, secondaryAnimation, child) {\n    final slide = Tween(\n      begin: const Offset(0.2, 0.0),\n      end: Offset.zero,\n    ).chain(CurveTween(curve: forwardTransition));\n\n    final fade = Tween(\n      begin: 0.0,\n      end: 1.0,\n    ).chain(CurveTween(curve: forwardTransition));\n\n    return SlideTransition(\n      position: animation.drive(slide),\n      child: FadeTransition(opacity: animation.drive(fade), child: child),\n    );\n  };\n}\n"
  },
  {
    "path": "lib/utils/radar_color.dart",
    "content": "final List<String> dBZColors = [\n  '00ffff', // 0\n  '00ecff', //\n  '00daff', //\n  '00c8ff', //\n  '00b6ff', //\n  '00a3ff', // 5\n  '0091ff', //\n  '007fff', //\n  '006dff', //\n  '005bff', // 10\n  '0048ff', //\n  '0036ff', //\n  '0024ff', //\n  '0012ff', //\n  '0000ff', // 15\n  '00ff00', // 16\n  '00f400', // 17\n  '00e900', // 18\n  '00de00', //\n  '00d300', // 20\n  '00c800',\n  '00be00',\n  '00b400',\n  '00aa00',\n  '00a000', // 25\n  '009600',\n  '33ab00',\n  '66c000',\n  '99d500',\n  'ccea00', // 30\n  'ffff00',\n  'fff400',\n  'ffe900',\n  'ffde00',\n  'ffd300', // 35\n  'ffc800',\n  'ffb800',\n  'ffa800',\n  'ff9800',\n  'ff8800', // 40\n  'ff7800',\n  'ff6000',\n  'ff4800',\n  'ff3000',\n  'ff1800', // 45\n  'ff0000',\n  'f40000',\n  'e90000',\n  'de0000',\n  'd30000', // 50\n  'c80000',\n  'be0000',\n  'b40000',\n  'aa0000',\n  'a00000', // 55\n  '960000',\n  'ab0033',\n  'c00066',\n  'd50099',\n  'ea00cc', // 60\n  'ff00ff',\n  'ea00ff',\n  'd500ff',\n  'c000ff',\n  'ab00ff', // 65\n  '9600ff',\n];\n"
  },
  {
    "path": "lib/utils/serialization.dart",
    "content": "/// Utility functions for JSON serialization and deserialization.\n///\n/// These functions are designed to be used with `json_serializable`'s `@JsonKey` annotation to handle custom type\n/// conversions during JSON parsing and serialization. They provide consistent parsing logic for common data types that\n/// may come in different formats from JSON APIs.\nlibrary;\n\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:timezone/timezone.dart';\n\n/// Parses a boolean-like integer value from JSON.\n///\n/// Returns `true` if the value is `1` (as an integer) or `'1'` (as a string), `false` otherwise. This is useful for\n/// parsing boolean values that are represented as integers in JSON (common in some APIs).\n///\n/// Used with `@JsonKey(fromJson: parseBoolishInt)` in `json_serializable` models.\n///\n/// Example:\n/// ```dart\n/// @JsonKey(fromJson: parseBoolishInt)\n/// final bool? alert;\n/// ```\nbool parseBoolishInt(v) => v == 1 || v == '1';\n\n/// Parses a double value from JSON.\n///\n/// Converts the value to a string and then parses it as a double. This handles cases where numeric values may come as\n/// strings or numbers in JSON.\n///\n/// Used with `@JsonKey(fromJson: parseDouble)` in `json_serializable` models.\n///\n/// Example:\n/// ```dart\n/// @JsonKey(fromJson: parseDouble)\n/// final double temperature;\n/// ```\ndouble parseDouble(v) => double.parse(v.toString());\n\n/// Parses a timestamp (milliseconds since epoch) from JSON into a [TZDateTime].\n///\n/// Converts a timestamp value (as an integer or string) to a timezone-aware date-time object using the 'Asia/Taipei'\n/// timezone. The timestamp is expected to be in milliseconds since the Unix epoch.\n///\n/// Used with `@JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)` in `json_serializable` models.\n///\n/// Example:\n/// ```dart\n/// @JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)\n/// final TZDateTime time;\n/// ```\nTZDateTime parseDateTime(v) {\n  final value = v is int ? v : v.toString().asInt;\n  return value.asTZDateTime;\n}\n\n/// Converts a [TZDateTime] to a JSON-serializable integer timestamp.\n///\n/// Returns the milliseconds since the Unix epoch for the given [TZDateTime]. This is the inverse operation of\n/// [parseDateTime] and is used when serializing timezone-aware date-time objects to JSON.\n///\n/// Used with `@JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)` in `json_serializable` models.\n///\n/// Example:\n/// ```dart\n/// @JsonKey(fromJson: parseDateTime, toJson: dateTimeToJson)\n/// final TZDateTime time;\n/// ```\nint dateTimeToJson(TZDateTime v) => v.millisecondsSinceEpoch;\n"
  },
  {
    "path": "lib/utils/shader_selector.dart",
    "content": "import 'package:dpip/api/model/weather_schema.dart';\nimport 'package:dpip/widgets/fog_shader_background.dart';\nimport 'package:dpip/widgets/thunderstorm_shader_background.dart';\nimport 'package:flutter/material.dart';\n\ndouble parseVisibility(dynamic value) {\n  if (value == null) return 10.0;\n\n  if (value is num) {\n    final numValue = value.toDouble();\n    if (numValue < 0) return 10.0;\n    return numValue;\n  }\n\n  if (value is String) {\n    final str = value.trim();\n\n    if (str.isEmpty || str == '無觀測') return 10.0;\n\n    if (str.startsWith('<')) {\n      final numStr = str.substring(1).trim();\n      final num = double.tryParse(numStr);\n      if (num != null) {\n        return num * 0.5;\n      }\n      return 0.5;\n    }\n\n    if (str.contains('-')) {\n      final parts = str.split('-');\n      if (parts.length == 2) {\n        final min = double.tryParse(parts[0].trim());\n        final max = double.tryParse(parts[1].trim());\n        if (min != null && max != null) {\n          return (min + max) / 2.0;\n        }\n        if (min != null) return min;\n        if (max != null) return max;\n      }\n    }\n\n    final num = double.tryParse(str);\n    if (num != null) return num;\n  }\n\n  return 10.0;\n}\n\nclass ShaderConfig {\n  final bool showFog;\n  final bool showRain;\n  final bool showThunderstorm;\n  final double fogIntensity;\n\n  const ShaderConfig({\n    this.showFog = false,\n    this.showRain = false,\n    this.showThunderstorm = false,\n    this.fogIntensity = 0.0,\n  });\n}\n\nclass ShaderSelector {\n  static bool debugForceThunderstormFog = true;\n\n  static bool _isThunderstormCode(int code) {\n    return code == 103 ||\n        code == 104 ||\n        (code >= 114 && code <= 119) ||\n        code == 203 ||\n        code == 204 ||\n        (code >= 214 && code <= 219) ||\n        code == 303 ||\n        code == 304 ||\n        (code >= 314 && code <= 319);\n  }\n\n  static bool _isRainCode(int code) {\n    return code == 106 ||\n        code == 111 ||\n        code == 206 ||\n        code == 211 ||\n        code == 306 ||\n        code == 311 ||\n        code == 107 ||\n        code == 112 ||\n        code == 207 ||\n        code == 212 ||\n        code == 307 ||\n        code == 312;\n  }\n\n  static bool _isFogCode(int code) {\n    return code == 105 || code == 205 || code == 305;\n  }\n\n  static ShaderConfig selectShaderConfig(RealtimeWeather? weather) {\n    if (debugForceThunderstormFog) {\n      return const ShaderConfig(\n        showFog: false,\n        showRain: false,\n        showThunderstorm: false,\n        fogIntensity: 0.6,\n      );\n    }\n\n    if (weather == null) {\n      return const ShaderConfig();\n    }\n\n    final weatherCode = weather.data.weatherCode;\n    final rain = weather.data.rain;\n    final visibility = parseVisibility(weather.data.visibility);\n\n    final showThunderstorm = _isThunderstormCode(weatherCode);\n    final showRain = (_isRainCode(weatherCode) || rain > 0) && !showThunderstorm;\n    final showFog = visibility < 5.0 || _isFogCode(weatherCode);\n    final fogIntensity = showFog ? calculateFogIntensity(visibility) : 0.0;\n\n    return ShaderConfig(\n      showFog: showFog,\n      showRain: showRain,\n      showThunderstorm: showThunderstorm,\n      fogIntensity: fogIntensity,\n    );\n  }\n\n  static double calculateFogIntensity(double visibility) {\n    if (visibility < 1.0) {\n      return 1.0;\n    }\n    if (visibility >= 5.0) {\n      return 0.0;\n    }\n    return 1.0 - (visibility - 1.0) / 4.0;\n  }\n\n  static Widget buildShaderBackground({\n    required ShaderConfig config,\n    required String imagePath,\n    Widget? child,\n  }) {\n    if (config.showThunderstorm) {\n      if (config.showFog) {\n        return Stack(\n          children: [\n            Positioned.fill(\n              child: ThunderstormShaderBackground(\n                imagePath: imagePath,\n                animated: true,\n                lightningIntensity: 1.0,\n                rainAmount: 0.5,\n              ),\n            ),\n            Positioned.fill(\n              child: Opacity(\n                opacity: config.fogIntensity * 0.7,\n                child: FogShaderBackground(\n                  imagePath: imagePath,\n                  animated: true,\n                  intensity: 1.0,\n                  speed: 1.0,\n                ),\n              ),\n            ),\n            Positioned.fill(child: child ?? const SizedBox()),\n          ],\n        );\n      }\n      return ThunderstormShaderBackground(\n        imagePath: imagePath,\n        animated: true,\n        lightningIntensity: 1.0,\n        rainAmount: 0.3,\n        child: child,\n      );\n    }\n\n    if (config.showRain && config.showFog) {\n      final fogIntensity = config.fogIntensity * 0.5;\n      return Stack(\n        children: [\n          Positioned.fill(\n            child: ThunderstormShaderBackground(\n              imagePath: imagePath,\n              animated: true,\n              lightningIntensity: 0.0,\n              rainAmount: 0.3,\n            ),\n          ),\n          Positioned.fill(\n            child: FogShaderBackground(\n              imagePath: imagePath,\n              animated: true,\n              intensity: fogIntensity,\n              speed: 1.0,\n            ),\n          ),\n          Positioned.fill(child: child ?? const SizedBox()),\n        ],\n      );\n    }\n\n    if (config.showRain) {\n      return ThunderstormShaderBackground(\n        imagePath: imagePath,\n        animated: true,\n        lightningIntensity: 0.0,\n        rainAmount: 0.3,\n        child: child,\n      );\n    }\n\n    if (config.showFog) {\n      return FogShaderBackground(\n        imagePath: imagePath,\n        animated: true,\n        intensity: config.fogIntensity,\n        speed: 1.0,\n        child: child,\n      );\n    }\n\n    return Container(\n      decoration: BoxDecoration(\n        image: DecorationImage(\n          image: AssetImage(imagePath),\n          fit: BoxFit.cover,\n        ),\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/toast.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\n\nvoid showToast(BuildContext context, ToastWidget toast) {\n  final fToast = FToast();\n  fToast.init(context);\n  fToast.showToast(\n    child: toast,\n    toastDuration: const Duration(seconds: 3),\n    fadeDuration: Durations.short4,\n    gravity: ToastGravity.BOTTOM,\n    isDismissible: true,\n  );\n}\n\nclass ToastWidget extends StatelessWidget {\n  final List<Widget> children;\n  const ToastWidget({super.key, required this.children});\n\n  ToastWidget.text(String text, {super.key, Widget? icon})\n    : children = [\n        if (icon != null) icon,\n        if (icon != null) const SizedBox(width: 4),\n        Flexible(child: Text(text, textAlign: TextAlign.center)),\n      ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(9999),\n        color: context.colors.surfaceContainer,\n        border: Border.all(color: context.colors.outlineVariant),\n        boxShadow: kElevationToShadow[8],\n      ),\n      child: Row(mainAxisSize: MainAxisSize.min, children: children),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/wallpaper_selector.dart",
    "content": "class WallpaperSelector {\n  static String debugWallpaperPath = '';\n\n  static const List<String> dayWallpapers = [\n    'assets/wallpaper/day/autumn_park.jpg',\n    'assets/wallpaper/day/forest_pink_mist.jpg',\n    'assets/wallpaper/day/forest_sunray.jpg',\n    'assets/wallpaper/day/village_cherry_blossom.jpg',\n  ];\n\n  static const List<String> duskWallpapers = [\n    'assets/wallpaper/dusk/japanese_rooftop.jpg',\n    'assets/wallpaper/dusk/mountain_sunset.jpg',\n    'assets/wallpaper/dusk/suburb_sunset.jpg',\n    'assets/wallpaper/dusk/vending_machine.jpg',\n  ];\n\n  static const List<String> nightWallpapers = [\n    'assets/wallpaper/night/cabin_lightning.jpg',\n    'assets/wallpaper/night/city_rooftop_stars.jpg',\n    'assets/wallpaper/night/city_street_blue.jpg',\n    'assets/wallpaper/night/forest_moonlight.jpg',\n    'assets/wallpaper/night/halloween_forest.jpg',\n    'assets/wallpaper/night/japanese_street_moon.jpg',\n    'assets/wallpaper/night/lighthouse_coast.jpg',\n    'assets/wallpaper/night/town_hillside.jpg',\n  ];\n\n  static int _hashDate(DateTime date) {\n    final dayOfYear = date.difference(DateTime(date.year, 1, 1)).inDays + 1;\n    return (date.year * 1000 + dayOfYear) % 2147483647;\n  }\n\n  static String selectWallpaper(DateTime utc8Time) {\n    if (debugWallpaperPath.isNotEmpty) {\n      return debugWallpaperPath;\n    }\n\n    final hour = utc8Time.hour;\n    final dateHash = _hashDate(utc8Time);\n    final periodHash = hour >= 6 && hour < 18 ? 0 : (hour >= 18 && hour < 20 ? 1 : 2);\n    final combinedHash = (dateHash * 3 + periodHash) % 2147483647;\n\n    if (hour >= 6 && hour < 18) {\n      final index = combinedHash % dayWallpapers.length;\n      return dayWallpapers[index];\n    } else if (hour >= 18 && hour < 20) {\n      final index = combinedHash % duskWallpapers.length;\n      return duskWallpapers[index];\n    } else {\n      final index = combinedHash % nightWallpapers.length;\n      return nightWallpapers[index];\n    }\n  }\n\n  static DateTime getUtc8Time() {\n    final now = DateTime.now().toUtc();\n    return now.add(const Duration(hours: 8));\n  }\n}\n"
  },
  {
    "path": "lib/utils/weather_icon.dart",
    "content": "import 'package:dpip/core/i18n.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\n\nclass WeatherIcons {\n  WeatherIcons._();\n\n  static const Map<String, IconData> iconMap = {\n    'sunny': Symbols.sunny_rounded,\n    'nightlight': Symbols.nightlight_rounded,\n    'partly_cloudy_day': Symbols.partly_cloudy_day_rounded,\n    'partly_cloudy_night': Symbols.partly_cloudy_night_rounded,\n    'cloudy': Symbols.cloud_rounded,\n    'foggy': Symbols.foggy_rounded,\n    'rainy': Symbols.rainy_rounded,\n    'snowy': Symbols.ac_unit_rounded,\n    'rainy_snow': Symbols.weather_mix_rounded,\n    'thunderstorm': Symbols.thunderstorm_rounded,\n    'hail': Symbols.grain_rounded,\n    'unknown': Symbols.error_rounded,\n  };\n\n  static final Map<int, Map<String, dynamic>> weatherCodeMap = {\n    // Sunny (晴)\n    100: {\n      'icon': {'day': 'sunny', 'night': 'nightlight'},\n      'key': 'sunny',\n    },\n    101: {\n      'icon': {'day': 'sunny', 'night': 'nightlight'},\n      'key': 'sunny_haze',\n    },\n    102: {\n      'icon': {'day': 'sunny', 'night': 'nightlight'},\n      'key': 'sunny_mist',\n    },\n    103: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_lightning',\n    },\n    104: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_thunder',\n    },\n    105: {\n      'icon': {'day': 'foggy', 'night': 'foggy'},\n      'key': 'sunny_fog',\n    },\n    106: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'sunny_rain',\n    },\n    107: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'sunny_rain_snow',\n    },\n    108: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'sunny_heavy_snow',\n    },\n    109: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'sunny_snow_pellets',\n    },\n    110: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'sunny_ice_pellets',\n    },\n    111: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'sunny_showers',\n    },\n    112: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'sunny_rain_snow_showers',\n    },\n    113: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'sunny_hail',\n    },\n    114: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_thunderstorm',\n    },\n    115: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_thunder_snow',\n    },\n    116: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_thunder_hail',\n    },\n    117: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_heavy_thunderstorm',\n    },\n    118: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_heavy_thunder_hail',\n    },\n    119: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'sunny_thunder',\n    },\n\n    // Cloudy (多雲)\n    200: {\n      'icon': {'day': 'partly_cloudy_day', 'night': 'partly_cloudy_night'},\n      'key': 'cloudy',\n    },\n    201: {\n      'icon': {'day': 'partly_cloudy_day', 'night': 'partly_cloudy_night'},\n      'key': 'cloudy_haze',\n    },\n    202: {\n      'icon': {'day': 'partly_cloudy_day', 'night': 'partly_cloudy_night'},\n      'key': 'cloudy_mist',\n    },\n    203: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_lightning',\n    },\n    204: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_thunder',\n    },\n    205: {\n      'icon': {'day': 'foggy', 'night': 'foggy'},\n      'key': 'cloudy_fog',\n    },\n    206: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'cloudy_rain',\n    },\n    207: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'cloudy_rain_snow',\n    },\n    208: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'cloudy_heavy_snow',\n    },\n    209: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'cloudy_snow_pellets',\n    },\n    210: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'cloudy_ice_pellets',\n    },\n    211: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'cloudy_showers',\n    },\n    212: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'cloudy_rain_snow_showers',\n    },\n    213: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'cloudy_hail',\n    },\n    214: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_thunderstorm',\n    },\n    215: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_thunder_snow',\n    },\n    216: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_thunder_hail',\n    },\n    217: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_heavy_thunderstorm',\n    },\n    218: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_heavy_thunder_hail',\n    },\n    219: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'cloudy_thunder',\n    },\n\n    // Overcast (陰)\n    300: {\n      'icon': {'day': 'cloudy', 'night': 'cloudy'},\n      'key': 'overcast',\n    },\n    301: {\n      'icon': {'day': 'cloudy', 'night': 'cloudy'},\n      'key': 'overcast_haze',\n    },\n    302: {\n      'icon': {'day': 'cloudy', 'night': 'cloudy'},\n      'key': 'overcast_mist',\n    },\n    303: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_lightning',\n    },\n    304: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_thunder',\n    },\n    305: {\n      'icon': {'day': 'foggy', 'night': 'foggy'},\n      'key': 'overcast_fog',\n    },\n    306: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'overcast_rain',\n    },\n    307: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'overcast_rain_snow',\n    },\n    308: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'overcast_heavy_snow',\n    },\n    309: {\n      'icon': {'day': 'snowy', 'night': 'snowy'},\n      'key': 'overcast_snow_pellets',\n    },\n    310: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'overcast_ice_pellets',\n    },\n    311: {\n      'icon': {'day': 'rainy', 'night': 'rainy'},\n      'key': 'overcast_showers',\n    },\n    312: {\n      'icon': {'day': 'rainy_snow', 'night': 'rainy_snow'},\n      'key': 'overcast_rain_snow_showers',\n    },\n    313: {\n      'icon': {'day': 'hail', 'night': 'hail'},\n      'key': 'overcast_hail',\n    },\n    314: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_thunderstorm',\n    },\n    315: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_thunder_snow',\n    },\n    316: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_thunder_hail',\n    },\n    317: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_heavy_thunderstorm',\n    },\n    318: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_heavy_thunder_hail',\n    },\n    319: {\n      'icon': {'day': 'thunderstorm', 'night': 'thunderstorm'},\n      'key': 'overcast_thunder',\n    },\n  };\n\n  static IconData getWeatherIcon(int code, bool isDay) {\n    final weatherInfo = weatherCodeMap[code];\n    if (weatherInfo != null) {\n      final iconName = isDay ? weatherInfo['icon']['day'] : weatherInfo['icon']['night'];\n      return iconMap[iconName] ?? Symbols.error_rounded;\n    }\n    return Symbols.error_rounded;\n  }\n\n  static String getWeatherContent(BuildContext context, int code) {\n    final Map<int, String> iconLabel = {\n      0: '取得天氣異常'.i18n,\n      100: '晴'.i18n,\n      101: '晴有霾'.i18n,\n      102: '晴有靄'.i18n,\n      103: '晴有閃電'.i18n,\n      104: '晴天伴有雷'.i18n,\n      105: '晴有霧'.i18n,\n      106: '晴有雨'.i18n,\n      107: '晴有雨雪'.i18n,\n      108: '晴有大雪'.i18n,\n      109: '晴有雪珠'.i18n,\n      110: '晴有冰珠'.i18n,\n      111: '晴有陣雪'.i18n,\n      112: '晴陣雨雪'.i18n,\n      113: '晴有雹'.i18n,\n      114: '晴有雷雨'.i18n,\n      115: '晴有雷雪'.i18n,\n      116: '晴有雷雹'.i18n,\n      117: '晴大雷雨'.i18n,\n      118: '晴大雷雹'.i18n,\n      119: '晴天伴有雷'.i18n,\n      200: '多雲'.i18n,\n      201: '多雲有霾'.i18n,\n      202: '多雲有靄'.i18n,\n      203: '多雲有閃電'.i18n,\n      204: '多雲伴有雷'.i18n,\n      205: '多雲有霧'.i18n,\n      206: '多雲有雨'.i18n,\n      207: '多雲有雨雪'.i18n,\n      208: '多雲有大雪'.i18n,\n      209: '多雲有雪珠'.i18n,\n      210: '多雲有冰珠'.i18n,\n      211: '多雲有陣雪'.i18n,\n      212: '多雲陣雨雪'.i18n,\n      213: '多雲有雹'.i18n,\n      214: '多雲有雷雨'.i18n,\n      215: '多雲有雷雪'.i18n,\n      216: '多雲有雷雹'.i18n,\n      217: '多雲大雷雨'.i18n,\n      218: '多雲大雷雹'.i18n,\n      219: '多雲伴有雷'.i18n,\n      300: '陰'.i18n,\n      301: '陰有霾'.i18n,\n      302: '陰有靄'.i18n,\n      303: '陰有閃電'.i18n,\n      304: '陰天伴有雷'.i18n,\n      305: '陰有霧'.i18n,\n      306: '陰有雨'.i18n,\n      307: '陰有雨雪'.i18n,\n      308: '陰有大雪'.i18n,\n      309: '陰有雪珠'.i18n,\n      310: '陰有冰珠'.i18n,\n      311: '陰有陣雪'.i18n,\n      312: '陰陣雨雪'.i18n,\n      313: '陰有雹'.i18n,\n      314: '陰有雷雨'.i18n,\n      315: '陰有雷雪'.i18n,\n      316: '陰有雷雹'.i18n,\n      317: '陰大雷雨'.i18n,\n      318: '陰大雷雹'.i18n,\n      319: '陰天伴有雷'.i18n,\n      // 103: context.i18n.partly_cloudy,\n      // 106: context.i18n.cloudy,\n      // 109: context.i18n.overcast,\n      // 130: context.i18n.foggy,\n      // 163: context.i18n.patchy_rain_possible,\n      // 166: context.i18n.patchy_snow_possible,\n      // 169: context.i18n.patchy_sleet_possible,\n      // 172: context.i18n.patchy_freezing_drizzle_possible,\n      // 187: context.i18n.thundery_outbreaks_possible,\n      // 114: context.i18n.blowing_snow,\n      // 117: context.i18n.blizzard,\n      // 135: context.i18n.fog,\n      // 147: context.i18n.freezing_fog,\n      // 150: context.i18n.patchy_light_drizzle,\n      // 153: context.i18n.light_drizzle,\n      // 168: context.i18n.freezing_drizzle,\n      // 171: context.i18n.heavy_freezing_drizzle,\n      // 180: context.i18n.patchy_light_rain,\n      // 183: context.i18n.light_rain,\n      // 186: context.i18n.moderate_rain_at_times,\n      // 189: context.i18n.moderate_rain,\n      // 192: context.i18n.heavy_rain_at_times,\n      // 195: context.i18n.heavy_rain,\n      // 198: context.i18n.light_freezing_rain,\n      // 201: context.i18n.moderate_or_heavy_freezing_rain,\n      // 204: context.i18n.light_sleet,\n      // 207: context.i18n.moderate_or_heavy_sleet,\n      // 210: context.i18n.patchy_light_snow,\n      // 213: context.i18n.light_snow,\n      // 216: context.i18n.patchy_moderate_snow,\n      // 219: context.i18n.moderate_snow,\n      // 222: context.i18n.patchy_heavy_snow,\n      // 225: context.i18n.heavy_snow,\n      // 237: context.i18n.ice_pellets,\n      // 240: context.i18n.light_rain_shower,\n      // 243: context.i18n.moderate_or_heavy_rain_shower,\n      // 246: context.i18n.torrential_rain_shower,\n      // 249: context.i18n.light_sleet_showers,\n      // 252: context.i18n.moderate_or_heavy_sleet_showers,\n      // 255: context.i18n.light_snow_showers,\n      // 258: context.i18n.moderate_or_heavy_snow_showers,\n      // 261: context.i18n.light_showers_of_ice_pellets,\n      // 264: context.i18n.moderate_or_heavy_showers_of_ice_pellets,\n      // 273: context.i18n.patchy_light_rain_with_thunder,\n      // 276: context.i18n.moderate_or_heavy_rain_with_thunder,\n      // 279: context.i18n.patchy_light_snow_with_thunder,\n      // 282: context.i18n.moderate_or_heavy_snow_with_thunder,\n    };\n\n    return iconLabel[code] ?? 'Unknown weather';\n  }\n\n  static String mapNumberToWeather(int number) {\n    final weatherInfo = weatherCodeMap[number];\n    if (weatherInfo != null) {\n      return weatherInfo['key'] as String;\n    }\n    return 'unknown_weather';\n  }\n}\n"
  },
  {
    "path": "lib/widgets/blurred_container.dart",
    "content": "import 'dart:ui';\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass BlurredContainer extends StatelessWidget {\n  final Widget child;\n  final EdgeInsets padding;\n  final Color? shadowColor;\n  final double elevation;\n  final double sigma;\n\n  const BlurredContainer({\n    super.key,\n    required this.child,\n    this.padding = const EdgeInsets.all(8),\n    this.shadowColor,\n    this.elevation = 0,\n    this.sigma = 16,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: context.colors.surfaceContainer.withValues(alpha: 0.6),\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(8),\n        side: BorderSide(color: context.colors.outline.withValues(alpha: 0.2)),\n      ),\n      elevation: elevation,\n      shadowColor: shadowColor ?? context.colors.shadow.withValues(alpha: 0.4),\n      clipBehavior: Clip.antiAlias,\n      child: BackdropFilter(\n        filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),\n        child: Padding(padding: padding, child: child),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/chip/label_chip.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass LabelChip extends StatelessWidget {\n  final Color? backgroundColor;\n  final Color? foregroundColor;\n  final Color? outlineColor;\n  final String label;\n\n  const LabelChip({\n    super.key,\n    required this.label,\n    this.backgroundColor,\n    this.foregroundColor,\n    this.outlineColor,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.all(6),\n      decoration: BoxDecoration(\n        color: backgroundColor ?? context.colors.surface,\n        borderRadius: BorderRadius.circular(6),\n        border: Border.all(color: outlineColor ?? context.colors.outline),\n      ),\n      child: Text(\n        label,\n        style: context.theme.textTheme.labelMedium?.copyWith(\n          color: foregroundColor ?? context.colors.onSurface,\n          height: 1,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/error/region_out_of_service.dart",
    "content": "import 'package:dpip/router.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass RegionOutOfService extends StatelessWidget {\n  const RegionOutOfService({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(24),\n      child: Column(\n        children: [\n          Text('服務區域外，僅在臺灣各地可用', style: context.theme.textTheme.titleMedium),\n          const SizedBox(height: 8),\n          FilledButton(\n            child: const Text('設定'),\n            onPressed: () {\n              SettingsIndexRoute().push(context);\n              SettingsLocationRoute().push(context);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/fog_shader_background.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui' as ui;\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass FogShaderBackground extends StatefulWidget {\n  final Widget? child;\n\n  final bool animated;\n\n  final String imagePath;\n\n  final double intensity;\n\n  final double speed;\n\n  const FogShaderBackground({\n    super.key,\n    this.child,\n    this.animated = true,\n    this.imagePath = 'assets/wallpaper/night/city_rooftop_stars.jpg',\n    this.intensity = 0.3,\n    this.speed = 1.0,\n  });\n\n  @override\n  State<FogShaderBackground> createState() => _FogShaderBackgroundState();\n}\n\nclass _FogShaderBackgroundState extends State<FogShaderBackground>\n    with SingleTickerProviderStateMixin {\n  ui.FragmentShader? _shader;\n  ui.Image? _image;\n  late final AnimationController _controller;\n  int _startTime = 0;\n\n  double get _elapsedTime => (DateTime.now().millisecondsSinceEpoch - _startTime) / 1000;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = AnimationController(\n      duration: const Duration(milliseconds: 300),\n      vsync: this,\n    );\n    if (widget.animated) {\n      _controller.repeat();\n    }\n    _loadAssets();\n  }\n\n  Future<void> _loadAssets() async {\n    try {\n      final results = await Future.wait([\n        ui.FragmentProgram.fromAsset('shaders/fog.frag'),\n        _loadImage(widget.imagePath),\n      ]);\n\n      if (mounted) {\n        setState(() {\n          _shader = (results[0] as ui.FragmentProgram).fragmentShader();\n          _image = results[1] as ui.Image;\n        });\n      }\n    } catch (e) {\n      debugPrint('Failed to load fog shader: $e');\n    }\n  }\n\n  Future<ui.Image> _loadImage(String path) async {\n    final data = await rootBundle.load(path);\n    final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());\n    final frame = await codec.getNextFrame();\n    return frame.image;\n  }\n\n  @override\n  void didUpdateWidget(FogShaderBackground oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.animated != oldWidget.animated) {\n      widget.animated ? _controller.repeat() : _controller.stop();\n    }\n    if (widget.imagePath != oldWidget.imagePath ||\n        widget.intensity != oldWidget.intensity ||\n        widget.speed != oldWidget.speed) {\n      _loadAssets();\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    _shader?.dispose();\n    _image?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    _startTime = DateTime.now().millisecondsSinceEpoch;\n\n    if (_shader == null || _image == null) {\n      return Container(\n        color: const Color(0xFF1a1a2e),\n        child: widget.child,\n      );\n    }\n\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final size = Size(constraints.maxWidth, constraints.maxHeight);\n\n        _shader!\n          ..setFloat(1, size.width)\n          ..setFloat(2, size.height)\n          ..setFloat(3, widget.intensity)\n          ..setFloat(4, widget.speed);\n\n        return AnimatedBuilder(\n          animation: _controller,\n          builder: (context, _) {\n            _shader!\n              ..setFloat(0, _elapsedTime)\n              ..setImageSampler(0, _image!);\n\n            return CustomPaint(\n              size: size,\n              painter: _FogShaderPainter(_shader!, _image!, size),\n              child: widget.child,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _FogShaderPainter extends CustomPainter {\n  final ui.FragmentShader shader;\n  final ui.Image image;\n  final Size viewSize;\n\n  _FogShaderPainter(this.shader, this.image, this.viewSize);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final imageWidth = image.width.toDouble();\n    final imageHeight = image.height.toDouble();\n    final imageAspect = imageWidth / imageHeight;\n    final viewAspect = viewSize.width / viewSize.height;\n\n    double srcX, srcY, srcW, srcH;\n\n    if (imageAspect > viewAspect) {\n      srcH = imageHeight;\n      srcW = imageHeight * viewAspect;\n      srcX = (imageWidth - srcW) / 2;\n      srcY = 0;\n    } else {\n      srcW = imageWidth;\n      srcH = imageWidth / viewAspect;\n      srcX = 0;\n      srcY = (imageHeight - srcH) / 2;\n    }\n\n    final recorder = ui.PictureRecorder();\n    final tempCanvas = Canvas(recorder);\n\n    tempCanvas.save();\n    tempCanvas.translate(0, viewSize.height);\n    tempCanvas.scale(1, -1);\n    tempCanvas.drawImageRect(\n      image,\n      Rect.fromLTWH(srcX, srcY, srcW, srcH),\n      Rect.fromLTWH(0, 0, viewSize.width, viewSize.height),\n      Paint(),\n    );\n    tempCanvas.restore();\n\n    final picture = recorder.endRecording();\n    final croppedImage = picture.toImageSync(\n      viewSize.width.toInt(),\n      viewSize.height.toInt(),\n    );\n\n    shader.setImageSampler(0, croppedImage);\n\n    canvas\n      ..translate(size.width, size.height)\n      ..rotate(math.pi)\n      ..drawRect(\n        Rect.fromLTWH(0, 0, size.width, size.height),\n        Paint()..shader = shader,\n      );\n  }\n\n  @override\n  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;\n}\n"
  },
  {
    "path": "lib/widgets/home/event_list_route.dart",
    "content": "import 'package:dpip/api/model/history/history.dart';\nimport 'package:dpip/api/model/history/intensity_history.dart';\nimport 'package:dpip/api/model/history/report_history.dart';\nimport 'package:dpip/app/map/_lib/utils.dart';\nimport 'package:dpip/route/event_viewer/intensity.dart';\nimport 'package:dpip/route/event_viewer/thunderstorm.dart';\nimport 'package:dpip/router.dart';\nimport 'package:flutter/material.dart';\n\nbool shouldShowArrow(History item) {\n  return [\n    HistoryType.thunderstorm,\n    HistoryType.heavyRain,\n    HistoryType.extremelyHeavyRain,\n    HistoryType.torrentialRain,\n    HistoryType.extremelyTorrentialRain,\n    HistoryType.earthquake,\n    HistoryType.intensity,\n  ].contains(item.type);\n}\n\nvoid handleEventList(BuildContext context, History history) {\n  Widget? page;\n\n  switch (history.type) {\n    case HistoryType.thunderstorm:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.heavyRain:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.extremelyHeavyRain:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.torrentialRain:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.extremelyTorrentialRain:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.workAndClassSuspension:\n      page = ThunderstormPage(item: history);\n\n    case HistoryType.earthquake:\n      MapRoute(\n        layers: MapLayer.report.name,\n        report: (history as ReportHistory).addition.id,\n      ).push(context);\n\n    case HistoryType.intensity:\n      page = IntensityPage(item: history as IntensityHistory);\n\n    case HistoryType.seawave:\n    case HistoryType.unknown:\n      // 未實現的類型，不顯示詳情\n      return;\n  }\n\n  if (page == null) return;\n\n  Navigator.push(context, MaterialPageRoute(builder: (context) => page!));\n}\n"
  },
  {
    "path": "lib/widgets/home/forecast_weather_card.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/color_scheme.dart';\nimport 'package:flutter/material.dart';\n\nclass ForecastWeatherCard extends StatelessWidget {\n  final String time;\n  final int maxTemperature;\n  final int minTemperature;\n  final int rain;\n  final IconData icon;\n\n  const ForecastWeatherCard({\n    super.key,\n    required this.time,\n    required this.maxTemperature,\n    required this.minTemperature,\n    required this.rain,\n    required this.icon,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 4,\n      surfaceTintColor: context.colors.surfaceTint,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text(\n              time,\n              style: TextStyle(fontSize: 16, color: context.colors.primary),\n            ),\n            Row(\n              children: [\n                Text(\n                  '$maxTemperature°',\n                  style: TextStyle(\n                    fontSize: 18,\n                    fontWeight: FontWeight.w500,\n                    color: context.colors.onSurface,\n                  ),\n                ),\n                Text(\n                  '/$minTemperature°',\n                  style: TextStyle(\n                    fontSize: 18,\n                    fontWeight: FontWeight.w500,\n                    color: context.colors.onSurfaceVariant,\n                  ),\n                ),\n              ],\n            ),\n            Text(\n              '$rain%',\n              style: TextStyle(\n                fontSize: 16,\n                color: context.theme.extendedColors.blue,\n              ),\n            ),\n            const SizedBox(height: 8),\n            Icon(\n              icon,\n              fill: 1,\n              size: 36,\n              color: context.colors.onPrimaryContainer.withValues(alpha: 0.75),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/layout.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nclass _VerticalLayout {\n  final MainAxisAlignment mainAxisAlignment;\n  final CrossAxisAlignment crossAxisAlignment;\n  final MainAxisSize mainAxisSize;\n  final TextBaseline? textBaseline;\n  final TextDirection? textDirection;\n  final VerticalDirection verticalDirection;\n  final double spacing;\n\n  const _VerticalLayout({\n    this.mainAxisAlignment = MainAxisAlignment.start,\n    this.mainAxisSize = MainAxisSize.max,\n    this.crossAxisAlignment = CrossAxisAlignment.center,\n    this.textDirection,\n    this.verticalDirection = VerticalDirection.down,\n    this.textBaseline,\n    this.spacing = 0.0,\n  });\n\n  _VerticalLayout copyWith({\n    MainAxisAlignment? mainAxisAlignment,\n    MainAxisSize? mainAxisSize,\n    CrossAxisAlignment? crossAxisAlignment,\n    TextDirection? textDirection,\n    VerticalDirection? verticalDirection,\n    TextBaseline? textBaseline,\n    double? spacing,\n  }) {\n    return _VerticalLayout(\n      mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment,\n      mainAxisSize: mainAxisSize ?? this.mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment,\n      textDirection: textDirection ?? this.textDirection,\n      verticalDirection: verticalDirection ?? this.verticalDirection,\n      textBaseline: textBaseline ?? this.textBaseline,\n      spacing: spacing ?? this.spacing,\n    );\n  }\n\n  Widget call({Key? key, required List<Widget> children, EdgeInsets? padding}) {\n    final widget = Column(\n      key: key,\n      mainAxisAlignment: mainAxisAlignment,\n      mainAxisSize: mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment,\n      textDirection: textDirection,\n      verticalDirection: verticalDirection,\n      textBaseline: textBaseline,\n      spacing: spacing,\n      children: children,\n    );\n\n    if (padding != null) {\n      return Padding(padding: padding, child: widget);\n    }\n\n    return widget;\n  }\n\n  _VerticalLayout get top => copyWith(mainAxisAlignment: MainAxisAlignment.start);\n  _VerticalLayout get bottom => copyWith(mainAxisAlignment: MainAxisAlignment.end);\n  _VerticalLayout get center => copyWith(mainAxisAlignment: MainAxisAlignment.center);\n  _VerticalLayout get left => copyWith(crossAxisAlignment: CrossAxisAlignment.start);\n  _VerticalLayout get right => copyWith(crossAxisAlignment: CrossAxisAlignment.end);\n  _VerticalLayout get stretch => copyWith(crossAxisAlignment: CrossAxisAlignment.stretch);\n  _VerticalLayout get min => copyWith(mainAxisSize: MainAxisSize.min);\n  _VerticalLayout get max => copyWith(mainAxisSize: MainAxisSize.max);\n  _VerticalLayout get reverse => copyWith(verticalDirection: VerticalDirection.up);\n\n  /// Set the spacing between children.\n  _VerticalLayout operator [](double spacing) => copyWith(spacing: spacing);\n}\n\nclass VLayout {\n  const VLayout._();\n\n  static const base = _VerticalLayout();\n\n  static _VerticalLayout get top => base.top;\n  static _VerticalLayout get bottom => base.bottom;\n  static _VerticalLayout get center => base.center;\n  static _VerticalLayout get left => base.left;\n  static _VerticalLayout get right => base.right;\n  static _VerticalLayout get stretch => base.stretch;\n  static _VerticalLayout get min => base.min;\n  static _VerticalLayout get max => base.max;\n  static _VerticalLayout get reverse => base.reverse;\n\n  /// Creates a vertical array of children.\n  ///\n  /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then\n  /// [textBaseline] must not be null.\n  ///\n  /// The [textDirection] argument defaults to the ambient [Directionality], if\n  /// any. If there is no ambient directionality, and a text direction is going\n  /// to be necessary to disambiguate `start` or `end` values for the\n  /// [crossAxisAlignment], the [textDirection] must not be null.\n  static Column raw({\n    Key? key,\n    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,\n    MainAxisSize mainAxisSize = MainAxisSize.max,\n    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,\n    TextDirection? textDirection,\n    VerticalDirection verticalDirection = VerticalDirection.down,\n    TextBaseline? textBaseline,\n    double spacing = 0.0,\n    List<Widget> children = const <Widget>[],\n  }) {\n    return Column(\n      key: key,\n      mainAxisAlignment: mainAxisAlignment,\n      mainAxisSize: mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment,\n      textDirection: textDirection,\n      verticalDirection: verticalDirection,\n      textBaseline: textBaseline,\n      spacing: spacing,\n      children: children,\n    );\n  }\n}\n\nclass _HorizontalLayout {\n  final MainAxisAlignment mainAxisAlignment;\n  final CrossAxisAlignment crossAxisAlignment;\n  final MainAxisSize mainAxisSize;\n  final TextBaseline? textBaseline;\n  final TextDirection? textDirection;\n  final VerticalDirection verticalDirection;\n  final double spacing;\n\n  const _HorizontalLayout({\n    this.mainAxisAlignment = MainAxisAlignment.start,\n    this.mainAxisSize = MainAxisSize.max,\n    this.crossAxisAlignment = CrossAxisAlignment.center,\n    this.textDirection,\n    this.verticalDirection = VerticalDirection.down,\n    this.textBaseline,\n    this.spacing = 0.0,\n  });\n\n  _HorizontalLayout copyWith({\n    MainAxisAlignment? mainAxisAlignment,\n    MainAxisSize? mainAxisSize,\n    CrossAxisAlignment? crossAxisAlignment,\n    TextDirection? textDirection,\n    VerticalDirection? verticalDirection,\n    TextBaseline? textBaseline,\n    double? spacing,\n  }) {\n    return _HorizontalLayout(\n      mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment,\n      mainAxisSize: mainAxisSize ?? this.mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment,\n      textDirection: textDirection ?? this.textDirection,\n      verticalDirection: verticalDirection ?? this.verticalDirection,\n      textBaseline: textBaseline ?? this.textBaseline,\n      spacing: spacing ?? this.spacing,\n    );\n  }\n\n  Widget call({\n    Key? key,\n    Iterable<Widget> children = const [],\n    EdgeInsets? padding,\n  }) {\n    final widget = Row(\n      key: key,\n      mainAxisAlignment: mainAxisAlignment,\n      mainAxisSize: mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment,\n      textDirection: textDirection,\n      verticalDirection: verticalDirection,\n      textBaseline: textBaseline,\n      spacing: spacing,\n      children: children is List<Widget> ? children : children.toList(),\n    );\n\n    if (padding != null) {\n      return Padding(padding: padding, child: widget);\n    }\n\n    return widget;\n  }\n\n  _HorizontalLayout get left => copyWith(mainAxisAlignment: MainAxisAlignment.start);\n  _HorizontalLayout get right => copyWith(mainAxisAlignment: MainAxisAlignment.end);\n  _HorizontalLayout get between => copyWith(mainAxisAlignment: MainAxisAlignment.spaceBetween);\n  _HorizontalLayout get around => copyWith(mainAxisAlignment: MainAxisAlignment.spaceAround);\n  _HorizontalLayout get evenly => copyWith(mainAxisAlignment: MainAxisAlignment.spaceEvenly);\n  _HorizontalLayout get top => copyWith(crossAxisAlignment: CrossAxisAlignment.start);\n  _HorizontalLayout get bottom => copyWith(crossAxisAlignment: CrossAxisAlignment.end);\n  _HorizontalLayout get center => copyWith(crossAxisAlignment: CrossAxisAlignment.center);\n  _HorizontalLayout get stretch => copyWith(crossAxisAlignment: CrossAxisAlignment.stretch);\n  _HorizontalLayout get min => copyWith(mainAxisSize: MainAxisSize.min);\n  _HorizontalLayout get max => copyWith(mainAxisSize: MainAxisSize.max);\n  _HorizontalLayout get reverse => copyWith(verticalDirection: VerticalDirection.up);\n\n  /// Set the spacing between children.\n  _HorizontalLayout operator [](double spacing) => copyWith(spacing: spacing);\n}\n\nclass HLayout {\n  const HLayout._();\n\n  static const base = _HorizontalLayout();\n\n  static _HorizontalLayout get left => base.left;\n  static _HorizontalLayout get right => base.right;\n  static _HorizontalLayout get top => base.top;\n  static _HorizontalLayout get bottom => base.bottom;\n  static _HorizontalLayout get center => base.center;\n  static _HorizontalLayout get stretch => base.stretch;\n  static _HorizontalLayout get min => base.min;\n  static _HorizontalLayout get max => base.max;\n  static _HorizontalLayout get reverse => base.reverse;\n\n  /// Creates a horizontal array of children.\n  ///\n  /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then\n  /// [textBaseline] must not be null.\n  ///\n  /// The [textDirection] argument defaults to the ambient [Directionality], if\n  /// any. If there is no ambient directionality, and a text direction is going\n  /// to be necessary to determine the layout order (which is always the case\n  /// unless the row has no children or only one child) or to disambiguate\n  /// `start` or `end` values for the [mainAxisAlignment], the [textDirection]\n  /// must not be null.\n  static Row raw({\n    Key? key,\n    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,\n    MainAxisSize mainAxisSize = MainAxisSize.max,\n    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,\n    TextDirection? textDirection,\n    VerticalDirection verticalDirection = VerticalDirection.down,\n    TextBaseline? textBaseline,\n    double spacing = 0.0,\n    List<Widget> children = const <Widget>[],\n  }) {\n    return Row(\n      key: key,\n      mainAxisAlignment: mainAxisAlignment,\n      mainAxisSize: mainAxisSize,\n      crossAxisAlignment: crossAxisAlignment,\n      textDirection: textDirection,\n      verticalDirection: verticalDirection,\n      textBaseline: textBaseline,\n      spacing: spacing,\n      children: children,\n    );\n  }\n}\n\nclass Layout {\n  const Layout._();\n\n  static _VerticalLayout get col => VLayout.base;\n  static _HorizontalLayout get row => HLayout.base;\n}\n"
  },
  {
    "path": "lib/widgets/list/detail_field_tile.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/widgets.dart';\n\nclass DetailFieldTile extends StatelessWidget {\n  final String label;\n  final Widget child;\n\n  const DetailFieldTile({super.key, required this.label, required this.child});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      margin: const EdgeInsets.all(8),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          Text(\n            label,\n            style: TextStyle(\n              fontSize: 14,\n              color: context.colors.onSurfaceVariant,\n            ),\n          ),\n          const SizedBox(height: 4),\n          child,\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/rain_time_selector.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass RainTimeSelector extends StatefulWidget {\n  final Function(String, String) onSelectionChanged;\n  final Function() onTimeExpanded;\n  final List<String> timeList;\n\n  const RainTimeSelector({\n    super.key,\n    required this.onSelectionChanged,\n    required this.onTimeExpanded,\n    required this.timeList,\n  });\n\n  @override\n  State<RainTimeSelector> createState() => _RainTimeSelectorState();\n}\n\nclass _RainTimeSelectorState extends State<RainTimeSelector> with SingleTickerProviderStateMixin {\n  late String _selectedTimestamp;\n  late String _selectedInterval;\n  late ScrollController _timeScrollController;\n  late ScrollController _intervalScrollController;\n  final double _itemWidth = 80.0;\n  late AnimationController _animationController;\n  late Animation<double> _expandAnimation;\n  bool _isExpanded = false;\n  int _selectIndex = 8;\n\n  final List<String> _intervals = [\n    '3d',\n    '2d',\n    '24h',\n    '12h',\n    '6h',\n    '3h',\n    '1h',\n    '10m',\n    'now',\n  ];\n  List<String> get _intervalTranslations => [\n    '3 天',\n    '3 天',\n    '24 小時',\n    '12 小時',\n    '6 小時',\n    '3 小時',\n    '1 小時',\n    '10 分鐘',\n    '今日',\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    _selectedTimestamp = widget.timeList.last;\n    _selectedInterval = 'now'; // Default to now\n    _timeScrollController = ScrollController();\n    _intervalScrollController = ScrollController();\n    _animationController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n    _expandAnimation = CurvedAnimation(\n      parent: _animationController,\n      curve: Curves.easeInOut,\n    );\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _scrollToSelected();\n      _scrollToSelectedInterval();\n    });\n  }\n\n  DateTime _convertTimestamp(String timestamp) {\n    return DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp));\n  }\n\n  @override\n  void dispose() {\n    _timeScrollController.dispose();\n    _intervalScrollController.dispose();\n    _animationController.dispose();\n    super.dispose();\n  }\n\n  void _scrollToSelected() {\n    if (!_timeScrollController.hasClients) return;\n    final index = widget.timeList.indexOf(_selectedTimestamp);\n    if (index != -1) {\n      final totalWidth = _itemWidth * widget.timeList.length;\n      final viewportWidth = _timeScrollController.position.viewportDimension;\n      final maxScroll = _timeScrollController.position.maxScrollExtent;\n\n      double targetScroll = (index * _itemWidth) - (viewportWidth / 2) + (_itemWidth / 2);\n\n      targetScroll = targetScroll.clamp(0.0, maxScroll);\n\n      if (totalWidth - targetScroll - viewportWidth < _itemWidth) {\n        targetScroll = maxScroll;\n      }\n\n      _timeScrollController.animateTo(\n        targetScroll,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  void _scrollToSelectedInterval() {\n    if (!_intervalScrollController.hasClients) return;\n    final index = _intervals.indexOf(_selectedInterval);\n    if (index != -1) {\n      final totalWidth = _itemWidth * _intervals.length;\n      final viewportWidth = _intervalScrollController.position.viewportDimension;\n      final maxScroll = _intervalScrollController.position.maxScrollExtent;\n\n      double targetScroll = (index * _itemWidth) - (viewportWidth / 2) + (_itemWidth / 2);\n\n      targetScroll = targetScroll.clamp(0.0, maxScroll);\n\n      if (totalWidth - targetScroll - viewportWidth < _itemWidth) {\n        targetScroll = maxScroll;\n      }\n\n      _intervalScrollController.animateTo(\n        targetScroll,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  void _toggleExpanded() {\n    setState(() {\n      _isExpanded = !_isExpanded;\n      if (_isExpanded) {\n        widget.onTimeExpanded();\n        _animationController.forward();\n      } else {\n        _animationController.reverse();\n      }\n    });\n  }\n\n  Widget _buildTimeSelector() {\n    return SizedBox(\n      height: 64,\n      child: Card(\n        elevation: 4,\n        surfaceTintColor: context.colors.surfaceTint,\n        margin: EdgeInsets.zero,\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 3),\n          child: ListView.builder(\n            controller: _timeScrollController,\n            scrollDirection: Axis.horizontal,\n            itemCount: widget.timeList.length,\n            itemBuilder: (context, index) {\n              final timestamp = widget.timeList[index];\n              final time = _convertTimestamp(timestamp);\n              final isSelected = timestamp == _selectedTimestamp;\n              return SizedBox(\n                width: _itemWidth,\n                child: GestureDetector(\n                  onTap: () {\n                    setState(() {\n                      _selectedTimestamp = timestamp;\n                    });\n                    widget.onSelectionChanged(\n                      _selectedTimestamp,\n                      _selectedInterval,\n                    );\n                    _scrollToSelected();\n                  },\n                  child: AnimatedContainer(\n                    duration: const Duration(milliseconds: 200),\n                    alignment: Alignment.center,\n                    margin: const EdgeInsets.symmetric(\n                      horizontal: 3,\n                      vertical: 6,\n                    ),\n                    decoration: BoxDecoration(\n                      color: isSelected ? context.colors.secondary : Colors.transparent,\n                      borderRadius: BorderRadius.circular(8),\n                    ),\n                    child: Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          DateFormat('HH:mm').format(time),\n                          style: TextStyle(\n                            color: isSelected\n                                ? context.colors.onSecondary\n                                : context.colors.onSurface,\n                            fontWeight: FontWeight.bold,\n                            fontSize: 16,\n                          ),\n                        ),\n                        Text(\n                          DateFormat('MM/dd').format(time),\n                          style: TextStyle(\n                            color: isSelected\n                                ? context.colors.onSecondary\n                                : context.colors.onSurface.withValues(\n                                    alpha: 0.7,\n                                  ),\n                            fontSize: 12,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildIntervalSelector() {\n    return SizedBox(\n      height: 58,\n      child: Card(\n        elevation: 4,\n        surfaceTintColor: context.colors.surfaceTint,\n        margin: EdgeInsets.zero,\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 3),\n          child: ListView.builder(\n            controller: _intervalScrollController,\n            scrollDirection: Axis.horizontal,\n            itemCount: _intervals.length,\n            itemBuilder: (context, index) {\n              final interval = _intervals[index];\n              final translation = _intervalTranslations[index];\n              final isSelected = interval == _selectedInterval;\n              return SizedBox(\n                width: _itemWidth,\n                child: GestureDetector(\n                  onTap: () {\n                    setState(() {\n                      _selectedInterval = interval;\n                    });\n                    _selectIndex = index;\n                    widget.onSelectionChanged(\n                      _selectedTimestamp,\n                      _selectedInterval,\n                    );\n                    _scrollToSelectedInterval();\n                  },\n                  child: AnimatedContainer(\n                    duration: const Duration(milliseconds: 200),\n                    alignment: Alignment.center,\n                    margin: const EdgeInsets.symmetric(\n                      horizontal: 3,\n                      vertical: 6,\n                    ),\n                    decoration: BoxDecoration(\n                      color: isSelected ? context.colors.secondary : Colors.transparent,\n                      borderRadius: BorderRadius.circular(8),\n                    ),\n                    child: Text(\n                      translation,\n                      style: TextStyle(\n                        color: isSelected ? context.colors.onSecondary : context.colors.onSurface,\n                        fontWeight: FontWeight.bold,\n                        fontSize: 16,\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FilledButton.tonalIcon(\n            onPressed: _toggleExpanded,\n            label: Text(\n              \"${DateFormat(\"yyyy/MM/dd HH:mm\").format(_convertTimestamp(_selectedTimestamp))} (${_intervalTranslations[_selectIndex]})\",\n              style: const TextStyle(fontWeight: FontWeight.bold),\n            ),\n            icon: Icon(_isExpanded ? Icons.expand_more : Icons.expand_less),\n            iconAlignment: IconAlignment.end,\n            style: ButtonStyle(\n              backgroundColor: WidgetStatePropertyAll(context.colors.surface),\n              foregroundColor: WidgetStatePropertyAll(context.colors.onSurface),\n              surfaceTintColor: WidgetStatePropertyAll(\n                context.colors.surfaceTint,\n              ),\n              padding: const WidgetStatePropertyAll(\n                EdgeInsets.fromLTRB(16, 0, 12, 0),\n              ),\n              elevation: const WidgetStatePropertyAll(4),\n            ),\n          ),\n          const SizedBox(height: 8),\n          FadeTransition(\n            opacity: _expandAnimation,\n            child: SizeTransition(\n              sizeFactor: _expandAnimation,\n              child: Padding(\n                padding: const EdgeInsets.only(bottom: 8),\n                child: Column(\n                  children: [\n                    _buildTimeSelector(),\n                    const SizedBox(height: 8),\n                    _buildIntervalSelector(),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/segmented_list.dart",
    "content": "/// Provides section-based list widgets that recreate the Android 16 design.\n///\n/// This library includes [SegmentedList] and [SliverSegmentedList] for creating labeled\n/// groups of widgets, [SegmentedListTile] for individual list items with\n/// the Android 16 styling, and [SectionText] for informational text blocks.\nlibrary;\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\n/// A section widget that groups children under an optional label.\n///\n/// This widget recreates the section layout from the Android 16,\n/// displaying a list of child widgets within a section, optionally preceded by\n/// a styled label. The label uses the primary color theme and appears with\n/// padding above the children.\n///\n/// Example:\n/// ```dart\n/// Section(\n///   label: Text('Settings'),\n///   children: [\n///     SectionListTile(title: Text('Option 1')),\n///     SectionListTile(title: Text('Option 2')),\n///   ],\n/// )\n/// ```\nclass SegmentedList extends StatelessWidget {\n  /// The optional label displayed at the top of the section.\n  final Widget? label;\n\n  final List<Widget>? _children;\n  final int? _itemCount;\n  final IndexedWidgetBuilder? _itemBuilder;\n\n  /// Creates a section with an optional [label] and required [children].\n  const SegmentedList({\n    super.key,\n    this.label,\n    required List<Widget> children,\n  }) : _children = children,\n       _itemCount = null,\n       _itemBuilder = null;\n\n  /// Creates a section with an optional [label] using a builder callback.\n  ///\n  /// Equivalent to [SegmentedList] but lazily builds items on demand, which is\n  /// more efficient for long lists.\n  const SegmentedList.builder({\n    super.key,\n    this.label,\n    required int itemCount,\n    required IndexedWidgetBuilder itemBuilder,\n  }) : _children = null,\n       _itemCount = itemCount,\n       _itemBuilder = itemBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: .min,\n      crossAxisAlignment: .start,\n      children: [\n        if (label != null)\n          Padding(\n            padding: .fromLTRB(32, 32, 16, 8),\n            child: DefaultTextStyle(\n              style: context.texts.labelLarge!.copyWith(\n                fontWeight: .w600,\n                color: context.colors.primary,\n              ),\n              child: label!,\n            ),\n          ),\n        ListView.builder(\n          padding: .symmetric(horizontal: 16),\n          shrinkWrap: true,\n          physics: const NeverScrollableScrollPhysics(),\n          itemCount: _children?.length ?? _itemCount!,\n          itemBuilder: _children != null ? (context, index) => _children[index] : _itemBuilder!,\n        ),\n      ],\n    );\n  }\n}\n\n/// A sliver variant of [SegmentedList] for use in scrollable layouts.\n///\n/// This widget recreates the section layout from the Android 16\n/// within a [CustomScrollView] or similar scrollable widget, grouping children\n/// under an optional label. The label uses the primary color theme and appears\n/// with padding above the children.\n///\n/// Use this instead of [SegmentedList] when building custom scrollable layouts\n/// with slivers for better performance.\n///\n/// Example:\n/// ```dart\n/// CustomScrollView(\n///   slivers: [\n///     SliverSection(\n///       label: Text('Settings'),\n///       children: [\n///         SectionListTile(title: Text('Option 1')),\n///         SectionListTile(title: Text('Option 2')),\n///       ],\n///     ),\n///   ],\n/// )\n/// ```\nclass SliverSegmentedList extends StatelessWidget {\n  /// The optional label displayed at the top of the section.\n  final Widget? label;\n\n  /// The list of widgets to display in the section.\n  final List<Widget> children;\n\n  /// Creates a sliver section with an optional [label] and required [children].\n  const SliverSegmentedList({\n    super.key,\n    this.label,\n    required this.children,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final label = this.label;\n\n    return SliverMainAxisGroup(\n      slivers: [\n        if (label != null)\n          SliverToBoxAdapter(\n            child: Padding(\n              padding: .fromLTRB(32, 32, 16, 8),\n              child: DefaultTextStyle(\n                style: context.texts.labelLarge!.copyWith(\n                  fontWeight: .w600,\n                  color: context.colors.primary,\n                ),\n                child: label,\n              ),\n            ),\n          ),\n        SliverPadding(\n          padding: .symmetric(horizontal: 16),\n          sliver: SliverList.builder(\n            itemCount: children.length,\n            itemBuilder: (context, index) => children[index],\n          ),\n        ),\n      ],\n    );\n  }\n}\n\n/// A customizable list tile that recreates the Android 16 design.\n///\n/// This widget provides a flexible list item with support for leading and\n/// trailing widgets, title, subtitle, and additional content. It features\n/// rounded corners that adapt based on position (first/last in a list),\n/// matching the visual style of the Android 16.\n///\n/// The tile has a Material design with ripple effects when tapped, and uses\n/// the theme's surface container color by default.\n///\n/// Example:\n/// ```dart\n/// SectionListTile(\n///   leading: Icon(Icons.settings),\n///   title: Text('Settings'),\n///   subtitle: Text('Configure app preferences'),\n///   trailing: Icon(Icons.chevron_right),\n///   onTap: () => Navigator.push(...),\n/// )\n/// ```\nclass SegmentedListTile extends StatelessWidget {\n  /// An optional widget displayed at the start of the tile.\n  ///\n  /// Typically an icon, and is constrained to 28 pixels width.\n  final Widget? leading;\n\n  /// Additional label displayed above the title.\n  final Widget? label;\n\n  /// The primary content of the tile.\n  final Widget? title;\n\n  /// Additional description displayed below the title.\n  final Widget? subtitle;\n\n  /// Optional content displayed below the title/subtitle.\n  ///\n  /// This content is indented if a [leading] widget is present.\n  final Widget? content;\n\n  /// An optional widget displayed at the end of the tile.\n  ///\n  /// Typically used for actions or indicators like chevrons.\n  final Widget? trailing;\n\n  /// Whether this is the first tile in a section.\n  ///\n  /// When true, the top corners use a 20 pixel radius, otherwise, they use\n  /// a 4 pixel radius. Has no effect if [borderRadius] is provided.\n  final bool isFirst;\n\n  /// Whether this is the last tile in a section.\n  ///\n  /// When true, the bottom corners use a 20 pixel radius, otherwise, they use\n  /// a 4 pixel radius. Has no effect if [borderRadius] is provided.\n  final bool isLast;\n\n  /// Whether the tile is enabled.\n  final bool enabled;\n\n  /// Custom shape for the tile.\n  ///\n  /// When provided, this overrides the default rounded rectangle shape and the\n  /// [borderRadius] parameter. Use this when you need more control over the\n  /// tile's appearance, such as beveled corners or custom borders.\n  ///\n  /// If null, the tile uses a [RoundedRectangleBorder] with the [borderRadius]\n  /// value (or computed based on [isFirst] and [isLast]).\n  final ShapeBorder? shape;\n\n  /// Custom border radius for the tile.\n  ///\n  /// When null, the top corners will use a 20 pixel radius if [isFirst] is true\n  /// (4 otherwise), and bottom corners use a 20 pixel radius if [isLast] is\n  /// true (4 otherwise).\n  final BorderRadius? borderRadius;\n\n  /// Custom padding for the [content] area.\n  ///\n  /// If not specified, defaults to left padding of 32 when [leading] is present.\n  final EdgeInsetsGeometry? contentPadding;\n\n  /// The background color of the tile.\n  final Color? tileColor;\n\n  /// Called when the tile is tapped.\n  final VoidCallback? onTap;\n\n  /// Called when the tile is long pressed.\n  final VoidCallback? onLongPress;\n\n  /// Creates a section list tile.\n  const SegmentedListTile({\n    super.key,\n    this.leading,\n    this.label,\n    this.title,\n    this.subtitle,\n    this.content,\n    this.trailing,\n    this.isFirst = false,\n    this.isLast = false,\n    this.enabled = true,\n    this.shape,\n    this.borderRadius,\n    this.contentPadding,\n    this.tileColor,\n    this.onTap,\n    this.onLongPress,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final label = this.label;\n    final title = this.title;\n    final subtitle = this.subtitle;\n    final content = this.content;\n    final leading = this.leading;\n    final trailing = this.trailing;\n\n    final borderRadius =\n        this.borderRadius ??\n        BorderRadius.vertical(\n          top: isFirst ? .circular(20) : .circular(4),\n          bottom: isLast ? .circular(20) : .circular(4),\n        );\n\n    return Opacity(\n      opacity: enabled ? 1 : 0.4,\n      child: Padding(\n        padding: .symmetric(vertical: 1),\n        child: Material(\n          color: tileColor ?? context.colors.surfaceContainerHigh,\n          shape: shape ?? RoundedRectangleBorder(borderRadius: borderRadius),\n          clipBehavior: .antiAlias,\n          child: InkWell(\n            borderRadius: borderRadius,\n            highlightColor: context.colors.surfaceTint.withValues(alpha: .16),\n            onTap: enabled ? onTap : null,\n            onLongPress: enabled ? onLongPress : null,\n            child: Padding(\n              padding: .all(16),\n              child: Column(\n                mainAxisSize: .min,\n                crossAxisAlignment: .start,\n                spacing: 16,\n                children: [\n                  if (label != null || title != null || subtitle != null)\n                    Row(\n                      spacing: 8,\n                      children: [\n                        if (leading != null)\n                          Padding(\n                            padding: .only(right: 4),\n                            child: ConstrainedBox(\n                              constraints: const .new(minWidth: 28),\n                              child: leading,\n                            ),\n                          ),\n                        Expanded(\n                          child: Column(\n                            mainAxisSize: .min,\n                            crossAxisAlignment: .start,\n                            spacing: 2,\n                            children: [\n                              if (label != null)\n                                DefaultTextStyle(\n                                  style: context.texts.labelLarge!.copyWith(\n                                    color: context.colors.onSurfaceVariant,\n                                  ),\n                                  child: label,\n                                ),\n                              if (title != null)\n                                DefaultTextStyle(\n                                  style: context.texts.titleMedium!.copyWith(\n                                    fontWeight: .bold,\n                                  ),\n                                  child: title,\n                                ),\n                              if (subtitle != null)\n                                DefaultTextStyle(\n                                  style: context.texts.bodyMedium!.copyWith(\n                                    color: context.colors.onSurfaceVariant,\n                                  ),\n                                  child: subtitle,\n                                ),\n                            ],\n                          ),\n                        ),\n                        if (trailing != null) trailing,\n                      ],\n                    ),\n                  if (content != null)\n                    Padding(\n                      padding: contentPadding ?? (leading != null ? .only(left: 32) : .zero),\n                      child: DefaultTextStyle(\n                        style: context.texts.bodyMedium!,\n                        child: content,\n                      ),\n                    ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n/// A text widget for displaying informational content within a section.\n///\n/// This widget shows text with an optional leading icon (defaults to an info\n/// icon if not provided). It uses the theme's outline color for subtle,\n/// secondary text appearance.\n///\n/// Commonly used for help text, disclaimers, or informational notes within\n/// a section layout.\n///\n/// Example:\n/// ```dart\n/// SectionText(\n///   leading: Icon(Icons.warning),\n///   child: Text('This action cannot be undone'),\n/// )\n/// ```\nclass SectionText extends StatelessWidget {\n  /// An optional widget displayed above the text.\n  ///\n  /// Defaults to an info icon if not specified.\n  final Widget? leading;\n\n  /// The text content to display.\n  final Widget child;\n\n  /// Optional custom text style.\n  ///\n  /// This style is merged with the default body medium style.\n  final TextStyle? textStyle;\n\n  /// Creates a section text widget.\n  const SectionText({\n    super.key,\n    this.leading,\n    required this.child,\n    this.textStyle,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: .symmetric(horizontal: 24, vertical: 16),\n      child: Column(\n        mainAxisSize: .min,\n        crossAxisAlignment: .start,\n        spacing: 8,\n        children: [\n          leading ??\n              Icon(\n                Symbols.info_rounded,\n                color: context.colors.outline,\n              ),\n          DefaultTextStyle(\n            style: context.texts.bodyMedium!\n                .copyWith(color: context.colors.outline)\n                .merge(textStyle),\n            child: child,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/tile_group_header.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass ListTileGroupHeader extends StatelessWidget {\n  final String title;\n\n  const ListTileGroupHeader({super.key, required this.title});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(16),\n      child: Text(\n        title,\n        style: TextStyle(\n          color: context.colors.primary,\n          fontSize: 14,\n          fontWeight: FontWeight.bold,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/time_selector.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass TimeSelector extends StatefulWidget {\n  final Function(String) onTimeSelected;\n  final Function() onTimeExpanded;\n  final List<String> timeList;\n\n  const TimeSelector({\n    super.key,\n    required this.onTimeSelected,\n    required this.onTimeExpanded,\n    required this.timeList,\n  });\n\n  @override\n  State<TimeSelector> createState() => _TimeSelectorState();\n}\n\nclass _TimeSelectorState extends State<TimeSelector> with SingleTickerProviderStateMixin {\n  late String _selectedTimestamp;\n  late ScrollController _scrollController;\n  final double _itemWidth = 80.0;\n  late AnimationController _animationController;\n  late Animation<double> _expandAnimation;\n  bool _isExpanded = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _selectedTimestamp = widget.timeList.last;\n    _scrollController = ScrollController();\n    _animationController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n    _expandAnimation = CurvedAnimation(\n      parent: _animationController,\n      curve: Curves.easeInOut,\n    );\n    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToSelected());\n  }\n\n  DateTime _convertTimestamp(String timestamp) {\n    return DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp));\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    _animationController.dispose();\n    super.dispose();\n  }\n\n  void _scrollToSelected() {\n    final index = widget.timeList.indexOf(_selectedTimestamp);\n    if (index != -1) {\n      final selectedItemOffset = index * _itemWidth;\n      final screenWidth = context.dimension.width;\n      final scrollOffset = selectedItemOffset - (screenWidth / 2) + (_itemWidth / 2);\n\n      _scrollController.animateTo(\n        scrollOffset.clamp(0.0, _scrollController.position.maxScrollExtent),\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  void _toggleExpanded() {\n    setState(() {\n      _isExpanded = !_isExpanded;\n      if (_isExpanded) {\n        widget.onTimeExpanded();\n        _animationController.forward();\n      } else {\n        _animationController.reverse();\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        FilledButton.tonalIcon(\n          onPressed: _toggleExpanded,\n          label: Text(\n            DateFormat(\n              'yyyy/MM/dd HH:mm',\n            ).format(_convertTimestamp(_selectedTimestamp)),\n            style: const TextStyle(fontWeight: FontWeight.bold),\n          ),\n          icon: Icon(_isExpanded ? Icons.expand_more : Icons.expand_less),\n          iconAlignment: IconAlignment.end,\n          style: ButtonStyle(\n            backgroundColor: WidgetStatePropertyAll(context.colors.surface),\n            foregroundColor: WidgetStatePropertyAll(context.colors.onSurface),\n            surfaceTintColor: WidgetStatePropertyAll(\n              context.colors.surfaceTint,\n            ),\n            padding: const WidgetStatePropertyAll(\n              EdgeInsets.fromLTRB(16, 0, 12, 0),\n            ),\n            elevation: const WidgetStatePropertyAll(2),\n          ),\n        ),\n        FadeTransition(\n          opacity: _expandAnimation,\n          child: SizeTransition(\n            sizeFactor: _expandAnimation,\n            child: SizedBox(\n              height: 76,\n              child: Card(\n                margin: const EdgeInsets.all(8),\n                elevation: 2,\n                surfaceTintColor: context.colors.surfaceTint,\n                shape: RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(12),\n                ),\n                child: Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 2),\n                  child: ListView.builder(\n                    controller: _scrollController,\n                    scrollDirection: Axis.horizontal,\n                    itemCount: widget.timeList.length,\n                    itemBuilder: (context, index) {\n                      final timestamp = widget.timeList[index];\n                      final time = _convertTimestamp(timestamp);\n                      final isSelected = timestamp == _selectedTimestamp;\n                      return SizedBox(\n                        width: _itemWidth,\n                        child: GestureDetector(\n                          onTap: () {\n                            setState(() {\n                              _selectedTimestamp = timestamp;\n                            });\n                            widget.onTimeSelected(_selectedTimestamp);\n                            _scrollToSelected();\n                          },\n                          child: AnimatedContainer(\n                            duration: const Duration(milliseconds: 200),\n                            alignment: Alignment.center,\n                            margin: const EdgeInsets.symmetric(\n                              horizontal: 3,\n                              vertical: 6,\n                            ),\n                            decoration: BoxDecoration(\n                              color: isSelected ? context.colors.secondary : Colors.transparent,\n                              borderRadius: BorderRadius.circular(8),\n                            ),\n                            child: Column(\n                              mainAxisAlignment: MainAxisAlignment.center,\n                              children: [\n                                AnimatedDefaultTextStyle(\n                                  duration: const Duration(milliseconds: 200),\n                                  style: TextStyle(\n                                    color: isSelected\n                                        ? context.colors.onSecondary\n                                        : context.colors.onSurface,\n                                    fontWeight: FontWeight.bold,\n                                    fontSize: 16,\n                                  ),\n                                  child: Text(DateFormat('HH:mm').format(time)),\n                                ),\n                                AnimatedDefaultTextStyle(\n                                  duration: const Duration(milliseconds: 200),\n                                  style: TextStyle(\n                                    color: isSelected\n                                        ? context.colors.onSecondary\n                                        : context.colors.onSurface.withValues(\n                                            alpha: 0.7,\n                                          ),\n                                    fontSize: 12,\n                                  ),\n                                  child: Text(DateFormat('MM/dd').format(time)),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      );\n                    },\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/timeline_tile.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass TimeLineTile extends StatelessWidget {\n  final DateTime time;\n  final Widget icon;\n  final Color color;\n  final Widget child;\n  final bool showDate;\n  final double height;\n  final bool first;\n  final void Function()? onTap;\n\n  const TimeLineTile({\n    super.key,\n    required this.time,\n    required this.icon,\n    required this.color,\n    required this.child,\n    this.showDate = false,\n    this.height = 88,\n    this.first = false,\n    this.onTap,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      borderRadius: BorderRadius.circular(16),\n      onTap: onTap,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(8),\n            child: SizedBox(\n              width: 84,\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.end,\n                children: [\n                  if (showDate)\n                    Text(\n                      DateFormat('yyyy/MM/dd').format(time),\n                      textAlign: TextAlign.right,\n                      style: TextStyle(\n                        color: context.colors.onSurfaceVariant,\n                        fontSize: 12,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                  Text(\n                    DateFormat('HH:mm:ss').format(time),\n                    textAlign: TextAlign.right,\n                    style: TextStyle(\n                      color: context.colors.onSurfaceVariant,\n                      fontSize: 12,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: Stack(\n              alignment: Alignment.center,\n              children: [\n                if (first)\n                  Padding(\n                    padding: EdgeInsets.only(top: height / 2),\n                    child: Container(\n                      width: 2,\n                      height: first ? height / 2 : height,\n                      color: context.colors.outlineVariant, // Color of the vertical line\n                    ),\n                  )\n                else\n                  Container(\n                    width: 2,\n                    height: first ? height / 2 : height,\n                    color: context.colors.outlineVariant, // Color of the vertical line\n                  ),\n                Container(\n                  height: 42,\n                  width: 42,\n                  decoration: BoxDecoration(\n                    color: color,\n                    shape: BoxShape.circle,\n                  ),\n                  child: Center(child: icon),\n                ),\n              ],\n            ),\n          ),\n          Expanded(\n            child: Padding(padding: const EdgeInsets.all(8), child: child),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/list/typhoon_time_selector.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass TyphoonTimeSelector extends StatefulWidget {\n  final Function(String, int) onSelectionChanged;\n  final Function() onTimeExpanded;\n  final List<String> timeList;\n  final List<String> typhoonList;\n  final List<int> typhoonIdList;\n  final int selectedTyphoonId;\n\n  const TyphoonTimeSelector({\n    super.key,\n    required this.onSelectionChanged,\n    required this.onTimeExpanded,\n    required this.timeList,\n    required this.typhoonList,\n    required this.selectedTyphoonId,\n    required this.typhoonIdList,\n  });\n\n  @override\n  State<TyphoonTimeSelector> createState() => _TyphoonTimeSelectorState();\n}\n\nclass _TyphoonTimeSelectorState extends State<TyphoonTimeSelector>\n    with SingleTickerProviderStateMixin {\n  late String _selectedTimestamp;\n  late int _selectedTyphoonId;\n  late ScrollController _timeScrollController;\n  late ScrollController _typhoonScrollController;\n  final double _itemWidth = 80.0;\n  late AnimationController _animationController;\n  late Animation<double> _expandAnimation;\n  bool _isExpanded = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _selectedTimestamp = widget.timeList.last;\n    _selectedTyphoonId = widget.selectedTyphoonId;\n    _timeScrollController = ScrollController();\n    _typhoonScrollController = ScrollController();\n    _animationController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n    _expandAnimation = CurvedAnimation(\n      parent: _animationController,\n      curve: Curves.easeInOut,\n    );\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _scrollToSelected();\n      _scrollToSelectedTyphoon();\n    });\n  }\n\n  DateTime _convertTimestamp(String timestamp) {\n    return DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp));\n  }\n\n  @override\n  void dispose() {\n    _timeScrollController.dispose();\n    _typhoonScrollController.dispose();\n    _animationController.dispose();\n    super.dispose();\n  }\n\n  void _scrollToSelected() {\n    if (!_timeScrollController.hasClients) return;\n    final index = widget.timeList.indexOf(_selectedTimestamp);\n    if (index != -1) {\n      final totalWidth = _itemWidth * widget.timeList.length;\n      final viewportWidth = _timeScrollController.position.viewportDimension;\n      final maxScroll = _timeScrollController.position.maxScrollExtent;\n\n      double targetScroll = (index * _itemWidth) - (viewportWidth / 2) + (_itemWidth / 2);\n\n      targetScroll = targetScroll.clamp(0.0, maxScroll);\n\n      if (totalWidth - targetScroll - viewportWidth < _itemWidth) {\n        targetScroll = maxScroll;\n      }\n\n      _timeScrollController.animateTo(\n        targetScroll,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  void _scrollToSelectedTyphoon() {\n    if (!_typhoonScrollController.hasClients) return;\n    final index = widget.typhoonIdList.indexOf(_selectedTyphoonId);\n    if (index != -1) {\n      final totalWidth = _itemWidth * widget.typhoonList.length;\n      final viewportWidth = _typhoonScrollController.position.viewportDimension;\n      final maxScroll = _typhoonScrollController.position.maxScrollExtent;\n\n      double targetScroll = (index * _itemWidth) - (viewportWidth / 2) + (_itemWidth / 2);\n\n      targetScroll = targetScroll.clamp(0.0, maxScroll);\n\n      if (totalWidth - targetScroll - viewportWidth < _itemWidth) {\n        targetScroll = maxScroll;\n      }\n\n      _typhoonScrollController.animateTo(\n        targetScroll,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n  }\n\n  void _toggleExpanded() {\n    setState(() {\n      _isExpanded = !_isExpanded;\n      if (_isExpanded) {\n        widget.onTimeExpanded();\n        _animationController.forward();\n      } else {\n        _animationController.reverse();\n      }\n    });\n  }\n\n  Widget _buildTimeSelector() {\n    return SizedBox(\n      height: 80,\n      child: Card(\n        margin: const EdgeInsets.all(8),\n        elevation: 4,\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n        child: ListView.builder(\n          controller: _timeScrollController,\n          scrollDirection: Axis.horizontal,\n          itemCount: widget.timeList.length,\n          itemBuilder: (context, index) {\n            final timestamp = widget.timeList[index];\n            final time = _convertTimestamp(timestamp);\n            final isSelected = timestamp == _selectedTimestamp;\n            return SizedBox(\n              width: _itemWidth,\n              child: GestureDetector(\n                onTap: () {\n                  setState(() {\n                    _selectedTimestamp = timestamp;\n                  });\n                  widget.onSelectionChanged(\n                    _selectedTimestamp,\n                    _selectedTyphoonId,\n                  );\n                  _scrollToSelected();\n                },\n                child: AnimatedContainer(\n                  duration: const Duration(milliseconds: 200),\n                  alignment: Alignment.center,\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 4,\n                    vertical: 8,\n                  ),\n                  decoration: BoxDecoration(\n                    color: isSelected ? context.colors.secondary : Colors.transparent,\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  child: Column(\n                    mainAxisAlignment: MainAxisAlignment.center,\n                    children: [\n                      Text(\n                        DateFormat('HH:mm').format(time),\n                        style: TextStyle(\n                          color: isSelected ? context.colors.onSecondary : context.colors.onSurface,\n                          fontWeight: FontWeight.bold,\n                          fontSize: 16,\n                        ),\n                      ),\n                      Text(\n                        DateFormat('MM/dd').format(time),\n                        style: TextStyle(\n                          color: isSelected\n                              ? context.colors.onSecondary\n                              : context.colors.onSurface.withValues(alpha: 0.7),\n                          fontSize: 12,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTyphoonSelector() {\n    return SizedBox(\n      height: 80,\n      child: Card(\n        margin: const EdgeInsets.all(8),\n        elevation: 4,\n        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),\n        child: ListView.builder(\n          controller: _typhoonScrollController,\n          scrollDirection: Axis.horizontal,\n          itemCount: widget.typhoonList.length,\n          itemBuilder: (context, index) {\n            final typhoonId = widget.typhoonIdList[index];\n            final isSelected = typhoonId == _selectedTyphoonId;\n            return SizedBox(\n              width: _itemWidth,\n              child: GestureDetector(\n                onTap: () {\n                  setState(() {\n                    _selectedTyphoonId = typhoonId;\n                  });\n                  widget.onSelectionChanged(\n                    _selectedTimestamp,\n                    _selectedTyphoonId,\n                  );\n                  _scrollToSelectedTyphoon();\n                },\n                child: AnimatedContainer(\n                  duration: const Duration(milliseconds: 200),\n                  alignment: Alignment.center,\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 4,\n                    vertical: 8,\n                  ),\n                  decoration: BoxDecoration(\n                    color: isSelected ? context.colors.secondary : Colors.transparent,\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  child: Text(\n                    widget.typhoonList[index],\n                    style: TextStyle(\n                      color: isSelected ? context.colors.onSecondary : context.colors.onSurface,\n                      fontWeight: FontWeight.bold,\n                      fontSize: 16,\n                    ),\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  String get _selectedTyphoonName {\n    final int index = widget.typhoonIdList.indexOf(_selectedTyphoonId);\n    if (index != -1 && index < widget.typhoonList.length) {\n      return widget.typhoonList[index];\n    }\n    return '未知';\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        GestureDetector(\n          onTap: _toggleExpanded,\n          child: Container(\n            margin: const EdgeInsets.only(bottom: 4, left: 16, right: 16),\n            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n            decoration: BoxDecoration(\n              color: context.colors.surface,\n              borderRadius: BorderRadius.circular(20),\n              boxShadow: [\n                BoxShadow(\n                  color: Colors.black.withValues(alpha: 0.1),\n                  blurRadius: 4,\n                  offset: const Offset(0, 2),\n                ),\n              ],\n            ),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                Text(\n                  \"${DateFormat(\"yyyy/MM/dd HH:mm\").format(_convertTimestamp(_selectedTimestamp))} ($_selectedTyphoonName)\",\n                  style: TextStyle(\n                    fontWeight: FontWeight.bold,\n                    color: context.colors.onSurface,\n                  ),\n                ),\n                const SizedBox(width: 4),\n                Icon(\n                  _isExpanded ? Icons.expand_more : Icons.expand_less,\n                  color: context.colors.onSurface,\n                ),\n              ],\n            ),\n          ),\n        ),\n        SizeTransition(\n          sizeFactor: _expandAnimation,\n          child: Column(\n            children: [_buildTimeSelector(), _buildTyphoonSelector()],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/map/intensity_legend.dart",
    "content": "import 'package:dpip/utils/instrumental_intensity_color.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:flutter/material.dart';\n\nenum IntensityLegendMode {\n  /// EEW mode: shows intensity 1-9\n  eew,\n\n  /// RTS mode: shows instrumental intensity -3 to 7\n  rts,\n}\n\nclass IntensityLegend extends StatelessWidget {\n  final IntensityLegendMode mode;\n\n  const IntensityLegend({super.key, this.mode = IntensityLegendMode.eew});\n\n  List<Color> get _colors => mode == IntensityLegendMode.eew\n      ? List.generate(9, (i) => IntensityColor.intensity(i + 1))\n      : List.generate(11, (i) => InstrumentalIntensityColor.intensity(7 - i));\n\n  List<String> get _labels => mode == IntensityLegendMode.eew\n      ? const ['1', '2', '3', '4', '5⁻', '5⁺', '6⁻', '6⁺', '7']\n      : const ['-3', '-2', '-1', '0', '1', '2', '3', '4', '5', '6', '7'];\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: _buildLegendItems(),\n    );\n  }\n\n  List<Widget> _buildLegendItems() {\n    final colors = mode == IntensityLegendMode.eew ? _colors.reversed.toList() : _colors;\n    final labels = _labels.reversed.toList();\n\n    if (mode == IntensityLegendMode.eew) {\n      return List.generate(colors.length, (index) {\n        return Row(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.center,\n          children: [\n            Container(\n              height: 12,\n              width: 8,\n              decoration: BoxDecoration(\n                color: colors[index],\n                borderRadius: BorderRadius.vertical(\n                  top: Radius.circular(index == 0 ? 4 : 0),\n                  bottom: Radius.circular(index == colors.length - 1 ? 4 : 0),\n                ),\n              ),\n            ),\n            const SizedBox(width: 6),\n            Text(\n              labels[index],\n              style: const TextStyle(fontSize: 10, height: 1),\n            ),\n          ],\n        );\n      });\n    } else {\n      final labelWidgets = labels.map((label) {\n        return Expanded(\n          child: Text(\n            label,\n            style: const TextStyle(fontSize: 9, height: 1),\n            textAlign: TextAlign.left,\n          ),\n        );\n      }).toList();\n\n      return [\n        Row(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Container(\n              height: 12.0 * colors.length,\n              width: 8,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(4),\n                gradient: LinearGradient(\n                  colors: colors,\n                  begin: Alignment.topCenter,\n                  end: Alignment.bottomCenter,\n                ),\n              ),\n            ),\n            const SizedBox(width: 6),\n            SizedBox(\n              height: 12.0 * colors.length,\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: labelWidgets,\n              ),\n            ),\n          ],\n        ),\n      ];\n    }\n  }\n}\n"
  },
  {
    "path": "lib/widgets/map/latlng_altitude.dart",
    "content": "import 'package:maplibre_gl/maplibre_gl.dart';\n\n/// A geographical location specified in degrees [latitude] and [longitude] with\n/// an altitude in meters above sea level.\n///\n/// This class extends [LatLng].\nclass LatLngAltitude extends LatLng {\n  /// Creates a geographical location specified in degrees [latitude] and\n  /// [longitude] with an altitude in meters above sea level.\n  ///\n  /// The latitude is clamped to the inclusive interval from -90.0 to +90.0.\n  ///\n  /// The longitude is normalized to the half-open interval from -180.0\n  /// (inclusive) to +180.0 (exclusive)\n  const LatLngAltitude(super.latitude, super.longitude, this.altitude);\n\n  /// The altitude in meters above sea level.\n  final double altitude;\n\n  @override\n  List<double> toGeoJsonCoordinates() => [latitude, longitude, altitude];\n\n  @override\n  String toString() => 'LatLngAltitude($latitude, $longitude, $altitude)';\n\n  @override\n  bool operator ==(Object other) {\n    return other is LatLngAltitude &&\n        other.latitude == latitude &&\n        other.longitude == longitude &&\n        other.altitude == altitude;\n  }\n\n  @override\n  int get hashCode => Object.hash(latitude, longitude, altitude);\n}\n"
  },
  {
    "path": "lib/widgets/map/legend.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass MapLegend extends StatelessWidget {\n  final List<Widget> children;\n  final String? label;\n\n  const MapLegend({super.key, required this.children, this.label});\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 4,\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(label ?? '圖例', style: context.theme.textTheme.titleMedium),\n            const SizedBox(height: 8),\n            ...children,\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/map/map.dart",
    "content": "import 'dart:math';\n\nimport 'package:async/async.dart';\nimport 'package:dpip/core/gps_location.dart';\nimport 'package:dpip/core/providers.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/utils/log.dart';\nimport 'package:dpip/widgets/map/style.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\n\nenum BaseMapType { exptech, osm, google }\n\nclass BaseMapSourceIds {\n  const BaseMapSourceIds._();\n\n  static const map = 'map';\n  static const userLocation = 'user-location';\n\n  static Iterable<String> values() sync* {\n    yield map;\n    yield userLocation;\n  }\n}\n\nclass BaseMapLayerIds {\n  const BaseMapLayerIds._();\n\n  static const exptechGlobalFill = 'exptech-global';\n  static const exptechTownFill = 'exptech-town';\n  static const exptechCountyFill = 'exptech-county';\n  static const exptechCountyOutline = 'exptech-county-outline';\n\n  static const userLocation = 'user-location';\n\n  static Iterable<String> values() sync* {\n    yield exptechGlobalFill;\n    yield exptechTownFill;\n    yield exptechCountyFill;\n    yield exptechCountyOutline;\n    yield userLocation;\n  }\n}\n\nclass DpipMap extends StatefulWidget {\n  final BaseMapType baseMapType;\n  final CameraPosition initialCameraPosition;\n  final void Function(MapLibreMapController controller)? onMapCreated;\n  final void Function(Point<double>, LatLng)? onMapClick;\n  final void Function()? onMapIdle;\n  final void Function(Point<double>, LatLng)? onMapLongClick;\n  final void Function()? onStyleLoadedCallback;\n  final MinMaxZoomPreference? minMaxZoomPreference;\n  final bool? rotateGesturesEnabled;\n  final bool? zoomGesturesEnabled;\n  final bool? doubleClickZoomEnabled;\n  final bool? dragEnabled;\n  final bool? scrollGesturesEnabled;\n  final bool? tiltGesturesEnabled;\n\n  /// Whether to set camera focus to user location when the user longitude or latitude is updated.\n  ///\n  /// Default is `false`.\n  final bool focusUserLocationWhenUpdated;\n\n  static const kTaiwanCenter = LatLng(23.60, 120.85);\n  static const kTaiwanZoom = 6.4;\n  static const kUserLocationZoom = 7.2;\n\n  const DpipMap({\n    super.key,\n    this.baseMapType = BaseMapType.exptech,\n    this.initialCameraPosition = const CameraPosition(\n      target: kTaiwanCenter,\n      zoom: kTaiwanZoom,\n    ),\n    this.onMapCreated,\n    this.onMapClick,\n    this.onMapIdle,\n    this.onMapLongClick,\n    this.onStyleLoadedCallback,\n    this.minMaxZoomPreference,\n    this.rotateGesturesEnabled,\n    this.zoomGesturesEnabled,\n    this.doubleClickZoomEnabled,\n    this.dragEnabled,\n    this.scrollGesturesEnabled,\n    this.tiltGesturesEnabled,\n    this.focusUserLocationWhenUpdated = false,\n  });\n\n  @override\n  State<DpipMap> createState() => DpipMapState();\n\n  static double adjustedZoom(BuildContext context, double zoom) {\n    final double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);\n    const double baseZoomAdjustment = 1.0;\n    const double mediumZoomAdjustment = 0.3;\n\n    if (devicePixelRatio >= 4.0) {\n      return zoom - baseZoomAdjustment;\n    } else if (devicePixelRatio >= 3.0) {\n      return zoom;\n    } else if (devicePixelRatio >= 2.0 && devicePixelRatio < 3.0) {\n      return zoom - mediumZoomAdjustment;\n    } else {\n      return zoom + baseZoomAdjustment;\n    }\n  }\n}\n\nclass DpipMapState extends State<DpipMap> {\n  MapLibreMapController? _controller;\n  Future<String>? _stylePathFuture;\n  bool _isMapReady = false;\n\n  Future<void> _updateUserLocation() async {\n    if (!mounted) return;\n\n    final controller = _controller;\n    if (controller == null) return;\n\n    try {\n      if (GlobalProviders.location.auto) {\n        await updateLocationFromGPS();\n      }\n\n      if (!mounted) return;\n\n      final location = GlobalProviders.location.coordinates;\n\n      final data = location?.toGeoJsonMap() ?? GeoJsonBuilder.empty;\n\n      await controller.setGeoJsonSource(BaseMapSourceIds.userLocation, data);\n\n      if (!mounted) return;\n\n      if (_isMapReady && widget.focusUserLocationWhenUpdated && location != null) {\n        try {\n          TalkerManager.instance.debug(\n            'DpipMap: animating to user location $location, zoom=${DpipMap.kUserLocationZoom}',\n          );\n          await Future.delayed(const Duration(milliseconds: 100));\n          if (!mounted) return;\n          await controller.animateCamera(\n            CameraUpdate.newLatLngZoom(location, DpipMap.kUserLocationZoom),\n            duration: const Duration(milliseconds: 500),\n          );\n          TalkerManager.instance.debug('DpipMap: animateCamera completed');\n        } catch (e) {\n          // 忽略相機動畫錯誤，可能是地圖還沒完全初始化\n          TalkerManager.instance.debug('地圖相機動畫失敗（可忽略）: $e');\n        }\n      } else {\n        TalkerManager.instance.debug(\n          'DpipMap._updateUserLocation: skipping animation, isMapReady=$_isMapReady, focusUserLocation=${widget.focusUserLocationWhenUpdated}, location=$location',\n        );\n      }\n    } catch (e, s) {\n      TalkerManager.instance.error('🗺️ failed to update user location', e, s);\n    }\n  }\n\n  void _initMap() {\n    if (_controller == null) return;\n    _isMapReady = true;\n    _updateUserLocation();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    GlobalProviders.location.$coordinates.addListener(_updateUserLocation);\n  }\n\n  ColorScheme? _lastColors;\n  CancelableOperation? _setThemeColorFuture;\n  Future<void> setThemeColors(ColorScheme colors) async {\n    final controller = _controller;\n    if (controller == null) return;\n\n    final layers = [\n      ...MapStyle.osmLayers(colors),\n      ...MapStyle.exptechLayers(colors),\n    ];\n\n    for (final layer in layers) {\n      if (layer['type'] == 'background') continue;\n\n      final json = layer['paint'] as Map<String, dynamic>;\n      json.remove('visibility');\n\n      final properties = switch (layer['type']) {\n        'fill' => FillLayerProperties.fromJson(json),\n        'line' => LineLayerProperties.fromJson(json),\n        'symbol' => SymbolLayerProperties.fromJson(json),\n        'raster' => RasterLayerProperties.fromJson(json),\n        _ => null,\n      };\n\n      await controller.setLayerProperties(layer['id'] as String, properties!);\n    }\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n\n    if (_stylePathFuture == null) {\n      _stylePathFuture = MapStyle(context, baseMap: widget.baseMapType).save();\n    } else if (_lastColors != context.colors) {\n      _setThemeColorFuture?.cancel();\n      _setThemeColorFuture = CancelableOperation.fromFuture(\n        setThemeColors(context.colors),\n      );\n    }\n\n    _lastColors = context.colors;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final double adjustedZoomValue = DpipMap.adjustedZoom(\n      context,\n      widget.initialCameraPosition.zoom,\n    );\n\n    return FutureBuilder(\n      future: _stylePathFuture,\n      builder: (context, snapshot) {\n        final styleString = snapshot.data;\n\n        if (snapshot.hasError) {\n          TalkerManager.instance.error(\n            'DpipMap: style load error',\n            snapshot.error,\n          );\n        }\n\n        if (styleString == null) {\n          TalkerManager.instance.debug(\n            'DpipMap: waiting for style, hasError=${snapshot.hasError}, connectionState=${snapshot.connectionState}',\n          );\n          return const Center(child: CircularProgressIndicator());\n        }\n\n        TalkerManager.instance.debug('DpipMap: style loaded, building map');\n\n        return ColoredBox(\n          color: context.colors.surface,\n          child: MapLibreMap(\n            minMaxZoomPreference:\n                widget.minMaxZoomPreference ?? const MinMaxZoomPreference(4, 11), // 不要動 雷達回波 會有問題\n            trackCameraPosition: true,\n            initialCameraPosition: CameraPosition(\n              target: widget.initialCameraPosition.target,\n              zoom: adjustedZoomValue,\n              bearing: widget.initialCameraPosition.bearing,\n              tilt: widget.initialCameraPosition.tilt,\n            ),\n            styleString: styleString,\n            tiltGesturesEnabled: widget.tiltGesturesEnabled ?? false,\n            scrollGesturesEnabled: widget.scrollGesturesEnabled ?? true,\n            rotateGesturesEnabled: widget.rotateGesturesEnabled ?? false,\n            zoomGesturesEnabled: widget.zoomGesturesEnabled ?? true,\n            doubleClickZoomEnabled: widget.doubleClickZoomEnabled ?? true,\n            dragEnabled: widget.dragEnabled ?? true,\n            attributionButtonMargins: const Point<double>(-100, -100),\n            onMapCreated: (controller) {\n              TalkerManager.instance.debug(\n                'DpipMap.onMapCreated: controller received',\n              );\n              _controller = controller;\n              widget.onMapCreated?.call(controller);\n            },\n            onMapClick: widget.onMapClick,\n            onMapIdle: widget.onMapIdle,\n            onMapLongClick: widget.onMapLongClick,\n            onStyleLoadedCallback: () {\n              TalkerManager.instance.debug(\n                'DpipMap.onStyleLoadedCallback: style loaded',\n              );\n              _initMap();\n              widget.onStyleLoadedCallback?.call();\n            },\n            translucentTextureSurface: true,\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    GlobalProviders.location.$coordinates.removeListener(_updateUserLocation);\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/widgets/map/style.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:dpip/utils/constants.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/latlng.dart';\nimport 'package:dpip/utils/geojson.dart';\nimport 'package:dpip/widgets/map/map.dart';\nimport 'package:flutter/material.dart';\nimport 'package:maplibre_gl/maplibre_gl.dart';\nimport 'package:path_provider/path_provider.dart';\n\nclass MapStyle {\n  late Map<String, dynamic> json;\n\n  MapStyle(BuildContext context, {required BaseMapType baseMap}) {\n    json = {\n      'version': 8,\n      'name': 'DPIP Map',\n      'center': DpipMap.kTaiwanCenter.asGeoJsonCooridnate,\n      'zoom': DpipMap.adjustedZoom(context, DpipMap.kTaiwanZoom),\n      'font-faces': {\n        'Noto Sans TC Regular':\n            'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf',\n        'Noto Sans TC Bold':\n            'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Bold.otf',\n      },\n      'glyphs': 'https://cdn.jsdelivr.net/gh/exptechtw/map-assets/{fontstack}/{range}.pbf',\n      'sprite': 'https://cdn.jsdelivr.net/gh/exptechtw/map-assets/sprites',\n      'sources': {\n        ...osmSource(),\n        ...googleSource(),\n        ...exptechSource(),\n        ...locationSource(),\n      },\n      'layers': [\n        background(),\n        ...osmLayers(context.colors, visible: baseMap == BaseMapType.osm),\n        ...googleLayers(visible: baseMap == BaseMapType.google),\n        ...exptechLayers(\n          context.colors,\n          visible: baseMap == BaseMapType.exptech,\n        ),\n        locationLayer(),\n      ],\n    };\n  }\n\n  Future<String> save() async {\n    final cache = await getApplicationCacheDirectory();\n    final cachePath = cache.path;\n\n    final data = jsonEncode(json);\n    final hash = md5.convert(utf8.encode(data)).toString();\n\n    final styleJsonFile = File('$cachePath/map-$hash.json');\n\n    if (!styleJsonFile.existsSync()) {\n      await styleJsonFile.writeAsString(data);\n    }\n\n    return styleJsonFile.path;\n  }\n\n  static Map<String, dynamic> osmSource() => {\n    'ne2_shaded': {\n      'maxzoom': 6,\n      'tileSize': 256,\n      'tiles': [\n        'https://tiles.openfreemap.org/natural_earth/ne2sr/{z}/{x}/{y}.png',\n      ],\n      'type': 'raster',\n    },\n    'openmaptiles': {\n      'type': 'vector',\n      'url': 'https://tiles.openfreemap.org/planet',\n      'volatile': true,\n    },\n  };\n\n  static Map<String, dynamic> background() => {\n    'id': 'background',\n    'type': 'background',\n    'paint': {'background-opacity': 0},\n    'layout': {'visibility': 'visible'},\n  };\n\n  static List<Map<String, dynamic>> osmLayers(\n    ColorScheme colors, {\n    bool visible = false,\n  }) => [\n    {\n      'id': 'osm-landcover-glacier',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'landcover',\n      'filter': [\n        '==',\n        ['get', 'subclass'],\n        'glacier',\n      ],\n      'paint': {\n        'fill-color': '#fff',\n        'fill-opacity': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          0,\n          0.9,\n          10,\n          0.3,\n        ],\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-park',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'park',\n      'filter': [\n        'match',\n        ['geometry-type'],\n        ['MultiPolygon', 'Polygon'],\n        true,\n        false,\n      ],\n      'paint': {\n        'fill-color': '#d8e8c8',\n        'fill-opacity': [\n          'interpolate',\n          ['exponential', 1.8],\n          ['zoom'],\n          9,\n          if (colors.brightness == Brightness.dark) 0.05 else 0.6,\n          12,\n          if (colors.brightness == Brightness.dark) 0.004 else 0.25,\n        ],\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-landcover-wood',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'landcover',\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'wood',\n      ],\n      'paint': {\n        'fill-antialias': [\n          'step',\n          ['zoom'],\n          false,\n          9,\n          true,\n        ],\n        'fill-color': '#66aa44',\n        'fill-opacity': 0.1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-landcover-grass',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'landcover',\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'grass',\n      ],\n      'paint': {\n        'fill-color': '#d2e8c2',\n        'fill-opacity': colors.brightness == Brightness.dark ? 0.2 : 1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-landcover-grass-park',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'park',\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'public_park',\n      ],\n      'paint': {\n        'fill-color': '#d8e8c8',\n        'fill-opacity': colors.brightness == Brightness.dark ? 0.4 : 0.8,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-waterway_tunnel',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'minzoom': 14,\n      'filter': [\n        'all',\n        [\n          'match',\n          ['get', 'class'],\n          ['canal', 'river', 'stream'],\n          true,\n          false,\n        ],\n        [\n          '==',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-dasharray': [2, 4],\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.3],\n          ['zoom'],\n          13,\n          0.5,\n          20,\n          6,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-other',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          'match',\n          ['get', 'class'],\n          ['canal', 'river', 'stream'],\n          false,\n          true,\n        ],\n        [\n          '==',\n          ['get', 'intermittent'],\n          0,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.3],\n          ['zoom'],\n          13,\n          0.5,\n          20,\n          2,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-other-intermittent',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          'match',\n          ['get', 'class'],\n          ['canal', 'river', 'stream'],\n          false,\n          true,\n        ],\n        [\n          '==',\n          ['get', 'intermittent'],\n          1,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-dasharray': [4, 3],\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.3],\n          ['zoom'],\n          13,\n          0.5,\n          20,\n          2,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-stream-canal',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          'match',\n          ['get', 'class'],\n          ['canal', 'stream'],\n          true,\n          false,\n        ],\n        [\n          '!=',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n        [\n          '==',\n          ['get', 'intermittent'],\n          0,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.3],\n          ['zoom'],\n          13,\n          0.5,\n          20,\n          6,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-stream-canal-intermittent',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          'match',\n          ['get', 'class'],\n          ['canal', 'stream'],\n          true,\n          false,\n        ],\n        [\n          '!=',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n        [\n          '==',\n          ['get', 'intermittent'],\n          1,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-dasharray': [4, 3],\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.3],\n          ['zoom'],\n          13,\n          0.5,\n          20,\n          6,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-river',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'river',\n        ],\n        [\n          '!=',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n        [\n          '!=',\n          ['get', 'intermittent'],\n          1,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.2],\n          ['zoom'],\n          10,\n          0.8,\n          20,\n          6,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-waterway-river-intermittent',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'river',\n        ],\n        [\n          '!=',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n        [\n          '==',\n          ['get', 'intermittent'],\n          1,\n        ],\n      ],\n      'paint': {\n        'line-color': '#a0c8f0',\n        'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1,\n        'line-dasharray': [3, 2.5],\n        'line-width': [\n          'interpolate',\n          ['exponential', 1.2],\n          ['zoom'],\n          10,\n          0.8,\n          20,\n          6,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-water',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'water',\n      'filter': [\n        'all',\n        [\n          '!=',\n          ['get', 'intermittent'],\n          1,\n        ],\n        [\n          '!=',\n          ['get', 'brunnel'],\n          'tunnel',\n        ],\n      ],\n      'paint': {\n        'fill-color': colors.brightness == Brightness.dark\n            ? colors.surfaceContainer.toHexStringRGB()\n            : '#AECFE2',\n        'fill-outline-color': colors.brightness == Brightness.dark\n            ? colors.outline.toHexStringRGB()\n            : '#AECFE2',\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-water-intermittent',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'water',\n      'filter': [\n        '==',\n        ['get', 'intermittent'],\n        1,\n      ],\n      'paint': {\n        'fill-color': '#CFE6F7',\n        'fill-opacity': colors.brightness == Brightness.dark ? 0.3 : 0.7,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-landcover-ice-shelf',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'landcover',\n      'filter': [\n        '==',\n        ['get', 'subclass'],\n        'ice_shelf',\n      ],\n      'paint': {\n        'fill-color': colors.brightness == Brightness.dark\n            ? colors.surfaceContainerHigh.toHexStringRGB()\n            : '#fff',\n        'fill-opacity': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          0,\n          0.9,\n          10,\n          0.3,\n        ],\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-landcover-sand',\n      'type': 'fill',\n      'source': 'openmaptiles',\n      'source-layer': 'landcover',\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'sand',\n      ],\n      'paint': {\n        'fill-color': '#f5eebc',\n        'fill-opacity': colors.brightness == Brightness.dark ? 0.6 : 1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-boundary_3',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'boundary',\n      'minzoom': 4,\n      'filter': [\n        'all',\n        [\n          '>=',\n          ['get', 'admin_level'],\n          3,\n        ],\n        [\n          '<=',\n          ['get', 'admin_level'],\n          6,\n        ],\n        [\n          '!=',\n          ['get', 'maritime'],\n          1,\n        ],\n        [\n          '!=',\n          ['get', 'disputed'],\n          1,\n        ],\n        [\n          '!',\n          ['has', 'claimed_by'],\n        ],\n      ],\n      'paint': {\n        'line-color': colors.outline.toHexStringRGB(),\n        'line-dasharray': [1, 1],\n        'line-width': [\n          'interpolate',\n          ['linear', 1],\n          ['zoom'],\n          7,\n          1,\n          11,\n          2,\n        ],\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-boundary_2',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'boundary',\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'admin_level'],\n          2,\n        ],\n        [\n          '!=',\n          ['get', 'maritime'],\n          1,\n        ],\n        [\n          '!=',\n          ['get', 'disputed'],\n          1,\n        ],\n        [\n          '!',\n          ['has', 'claimed_by'],\n        ],\n      ],\n      'paint': {\n        'line-color': colors.outlineVariant.toHexStringRGB(),\n        'line-opacity': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          0,\n          0.4,\n          4,\n          1,\n        ],\n        'line-width': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          3,\n          1,\n          5,\n          1.2,\n          12,\n          3,\n        ],\n      },\n      'layout': {\n        'line-cap': 'round',\n        'line-join': 'round',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-boundary_disputed',\n      'type': 'line',\n      'source': 'openmaptiles',\n      'source-layer': 'boundary',\n      'filter': [\n        'all',\n        [\n          '!=',\n          ['get', 'maritime'],\n          1,\n        ],\n        [\n          '==',\n          ['get', 'disputed'],\n          1,\n        ],\n      ],\n      'paint': {\n        'line-color': colors.outlineVariant.toHexStringRGB(),\n        'line-dasharray': [1, 2],\n        'line-width': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          3,\n          1,\n          5,\n          1.2,\n          12,\n          3,\n        ],\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': 'osm-waterway_line_label',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'waterway',\n      'minzoom': 10,\n      'filter': [\n        'match',\n        ['geometry-type'],\n        ['LineString', 'MultiLineString'],\n        true,\n        false,\n      ],\n      'paint': {\n        'text-color': '#74aee9',\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1.5,\n      },\n      'layout': {\n        'symbol-placement': 'line',\n        'symbol-spacing': 350,\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Regular'],\n        'text-letter-spacing': 0.2,\n        'text-max-width': 5,\n        'text-size': 14,\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-water_name_line_label',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'water_name',\n      'filter': [\n        'match',\n        ['geometry-type'],\n        ['LineString', 'MultiLineString'],\n        true,\n        false,\n      ],\n      'paint': {\n        'text-color': '#495e91',\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1.5,\n      },\n      'layout': {\n        'symbol-placement': 'line',\n        'symbol-spacing': 350,\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Regular'],\n        'text-letter-spacing': 0.2,\n        'text-max-width': 5,\n        'text-size': 14,\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_country_2',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'maxzoom': 9,\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'country',\n        ],\n        [\n          '==',\n          ['get', 'rank'],\n          2,\n        ],\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Bold'],\n        'text-max-width': 6.25,\n        'text-size': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          2,\n          9,\n          5,\n          17,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_town',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'minzoom': 6,\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'town',\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'icon-allow-overlap': true,\n        'icon-image': [\n          'step',\n          ['zoom'],\n          'circle_11_black',\n          10,\n          '',\n        ],\n        'icon-optional': false,\n        'icon-size': 0.2,\n        'text-anchor': 'bottom',\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Regular'],\n        'text-max-width': 8,\n        'text-size': [\n          'interpolate',\n          ['exponential', 1.2],\n          ['zoom'],\n          7,\n          12,\n          11,\n          14,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_state',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'minzoom': 5,\n      'maxzoom': 8,\n      'filter': [\n        '==',\n        ['get', 'class'],\n        'state',\n      ],\n      'paint': {\n        'text-color': colors.onSurfaceVariant.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Regular'],\n        'text-letter-spacing': 0.2,\n        'text-max-width': 9,\n        'text-size': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          5,\n          10,\n          8,\n          14,\n        ],\n        'text-transform': 'uppercase',\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_city',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'minzoom': 3,\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'city',\n        ],\n        [\n          '!=',\n          ['get', 'capital'],\n          2,\n        ],\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'icon-allow-overlap': true,\n        'icon-image': [\n          'step',\n          ['zoom'],\n          'circle_11_black',\n          9,\n          '',\n        ],\n        'icon-optional': false,\n        'icon-size': 0.4,\n        'text-anchor': 'bottom',\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Regular'],\n        'text-max-width': 8,\n        'text-offset': [0, -0.1],\n        'text-size': [\n          'interpolate',\n          ['exponential', 1.2],\n          ['zoom'],\n          4,\n          11,\n          7,\n          13,\n          11,\n          18,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_city_capital',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'minzoom': 3,\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'city',\n        ],\n        [\n          '==',\n          ['get', 'capital'],\n          2,\n        ],\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'icon-allow-overlap': true,\n        'icon-image': [\n          'step',\n          ['zoom'],\n          'circle_11_black',\n          9,\n          '',\n        ],\n        'icon-optional': false,\n        'icon-size': 0.5,\n        'text-anchor': 'bottom',\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Bold'],\n        'text-max-width': 8,\n        'text-offset': [0, -0.2],\n        'text-size': [\n          'interpolate',\n          ['exponential', 1.2],\n          ['zoom'],\n          4,\n          12,\n          7,\n          14,\n          11,\n          20,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_country_3',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'minzoom': 2,\n      'maxzoom': 9,\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'country',\n        ],\n        [\n          '>=',\n          ['get', 'rank'],\n          3,\n        ],\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Bold'],\n        'text-max-width': 6.25,\n        'text-size': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          3,\n          9,\n          7,\n          17,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n    {\n      'id': 'osm-label_country_1',\n      'type': 'symbol',\n      'source': 'openmaptiles',\n      'source-layer': 'place',\n      'maxzoom': 9,\n      'filter': [\n        'all',\n        [\n          '==',\n          ['get', 'class'],\n          'country',\n        ],\n        [\n          '==',\n          ['get', 'rank'],\n          1,\n        ],\n      ],\n      'paint': {\n        'text-color': colors.onSurface.toHexStringRGB(),\n        'text-halo-blur': 1,\n        'text-halo-color': colors.outlineVariant.toHexStringRGB(),\n        'text-halo-width': 1,\n      },\n      'layout': {\n        'text-field': [\n          'coalesce',\n          ['get', 'name:nonlatin'],\n          ['get', 'name'],\n        ],\n        'text-font': ['Noto Sans TC Bold'],\n        'text-max-width': 6.25,\n        'text-size': [\n          'interpolate',\n          ['linear'],\n          ['zoom'],\n          1,\n          9,\n          4,\n          17,\n        ],\n        'visibility': visible ? 'visible' : 'none',\n      },\n    },\n  ];\n\n  static Map<String, dynamic> googleSource() => {\n    'google': {\n      'type': 'raster',\n      'tiles': ['https://mt1.google.com/vt/lyrs=s&hl=zh-TW&x={x}&y={y}&z={z}'],\n      'tileSize': 256,\n      'attribution': '&copy; Google Maps',\n      'maxzoom': 19,\n    },\n  };\n\n  static List<Map<String, dynamic>> googleLayers({bool visible = false}) => [\n    {\n      'id': 'google-raster',\n      'type': 'raster',\n      'source': 'google',\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n  ];\n\n  static Map<String, dynamic> exptechSource() => {\n    'exptech': {\n      'type': 'vector',\n      'url': 'https://lb.exptech.dev/api/v1/map/tiles/tiles.json',\n      'tileSize': 512,\n      'buffer': 64,\n    },\n  };\n\n  static List<Map<String, dynamic>> exptechLayers(\n    ColorScheme colors, {\n    bool visible = false,\n  }) => [\n    {\n      'id': BaseMapLayerIds.exptechGlobalFill,\n      'type': 'fill',\n      'source': 'exptech',\n      'source-layer': 'global',\n      'paint': {\n        'fill-color': colors.surfaceContainer.toHexStringRGB(),\n        'fill-opacity': 1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': BaseMapLayerIds.exptechCountyFill,\n      'type': 'fill',\n      'source': 'exptech',\n      'source-layer': 'city',\n      'paint': {\n        'fill-color': colors.surfaceContainerHigh.toHexStringRGB(),\n        'fill-opacity': 1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': BaseMapLayerIds.exptechTownFill,\n      'type': 'fill',\n      'source': 'exptech',\n      'source-layer': 'town',\n      'paint': {\n        'fill-color': colors.surfaceContainerHigh.toHexStringRGB(),\n        'fill-opacity': 1,\n      },\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n    {\n      'id': BaseMapLayerIds.exptechCountyOutline,\n      'type': 'line',\n      'source': 'exptech',\n      'source-layer': 'city',\n      'paint': {'line-color': colors.outline.toHexStringRGB()},\n      'layout': {'visibility': visible ? 'visible' : 'none'},\n    },\n  ];\n\n  static Map<String, dynamic> locationSource() => {\n    'user-location': {'type': 'geojson', 'data': GeoJsonBuilder.empty},\n  };\n\n  static Map<String, dynamic> locationLayer() => {\n    'id': 'user-location',\n    'type': 'symbol',\n    'source': 'user-location',\n    'layout': {\n      'icon-image': 'gps',\n      'icon-size': kSymbolIconSize,\n      'icon-allow-overlap': true,\n      'icon-ignore-placement': true,\n    },\n  };\n}\n"
  },
  {
    "path": "lib/widgets/markdown.dart",
    "content": "/// Markdown rendering widget.\n///\n/// This file provides a customizable markdown renderer that supports:\n/// - Standard markdown syntax\n/// - Syntax highlighting for code blocks\n/// - Custom styled headings, links, and blockquotes\n/// - Copy-to-clipboard functionality for code blocks\nlibrary;\n\nimport 'dart:async';\n\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/google_fonts.dart';\nimport 'package:html_unescape/html_unescape_small.dart';\nimport 'package:markdown_widget/markdown_widget.dart' hide ImageViewer;\nimport 'package:material_symbols_icons/material_symbols_icons.dart';\nimport 'package:simple_icons/simple_icons.dart';\n\n/// Programming languages supported for syntax highlighting in code blocks.\n///\n/// Each language can optionally define an icon and color for visual\n/// identification in code block headers.\nenum Language {\n  /// The C programming language.\n  c(name: 'C', icon: SimpleIcons.c, iconColor: SimpleIconColors.c),\n\n  /// The C++ programming language.\n  cpp(\n    name: 'C++',\n    icon: SimpleIcons.cplusplus,\n    iconColor: SimpleIconColors.cplusplus,\n  ),\n\n  /// The C# programming language.\n  csharp(name: 'C#'),\n\n  /// The Dart programming language.\n  dart(name: 'Dart', icon: SimpleIcons.dart, iconColor: SimpleIconColors.dart),\n\n  /// The Java programming language.\n  java(name: 'Java'),\n\n  /// The JavaScript programming language.\n  javascript(\n    name: 'JavaScript',\n    icon: SimpleIcons.javascript,\n    iconColor: SimpleIconColors.javascript,\n  ),\n\n  /// The JavaScript programming language with XML syntax support (aka .jsx).\n  javascriptReact(\n    name: 'JavaScript React',\n    icon: SimpleIcons.react,\n    iconColor: SimpleIconColors.javascript,\n  ),\n\n  /// The Kotlin programming language.\n  kotlin(\n    name: 'Kotlin',\n    icon: SimpleIcons.kotlin,\n    iconColor: SimpleIconColors.kotlin,\n  ),\n\n  /// The Markdown markup language.\n  markdown(\n    name: 'Markdown',\n    icon: Symbols.markdown_rounded,\n    iconColor: SimpleIconColors.markdown,\n  ),\n\n  /// The Python programming language.\n  python(\n    name: 'Python',\n    icon: SimpleIcons.python,\n    iconColor: SimpleIconColors.python,\n  ),\n\n  /// The TypeScript programming language.\n  typescript(\n    name: 'TypeScript',\n    icon: SimpleIcons.typescript,\n    iconColor: SimpleIconColors.typescript,\n  ),\n\n  /// The TypeScript programming language with XML syntax support (aka .tsx).\n  typescriptReact(\n    name: 'TypeScript React',\n    icon: SimpleIcons.react,\n    iconColor: SimpleIconColors.react,\n  ),\n\n  /// The Vue framework language.\n  vue(\n    name: 'Vue',\n    icon: SimpleIcons.vuedotjs,\n    iconColor: SimpleIconColors.vuedotjs,\n  );\n\n  /// The display name of the language.\n  final String name;\n\n  /// The icon representing this language (optional).\n  final IconData? icon;\n\n  /// The color to apply to the icon (optional).\n  final Color? iconColor;\n\n  const Language({required this.name, this.icon, this.iconColor});\n}\n\n/// Resolves a language identifier string to a [Language] enum value.\n///\n/// Supports multiple aliases for each language (e.g., 'js', 'javascript').\n/// Returns `null` if the language is not recognized.\n///\n/// Example:\n/// ```dart\n/// resolveLanguage('js') // Returns Language.javascript\n/// resolveLanguage('tsx') // Returns Language.typescriptReact\n/// resolveLanguage('unknown') // Returns null\n/// ```\nLanguage? resolveLanguage(String language) {\n  switch (language) {\n    case 'c':\n      return Language.c;\n\n    case 'cpp':\n    case 'c++':\n      return Language.cpp;\n\n    case 'cs':\n    case 'csharp':\n      return Language.csharp;\n\n    case 'dart':\n      return Language.dart;\n\n    case 'java':\n      return Language.java;\n\n    case 'js':\n    case 'javascript':\n      return Language.javascript;\n\n    case 'jsx':\n    case 'javascriptreact':\n      return Language.javascriptReact;\n\n    case 'kt':\n    case 'kotlin':\n      return Language.kotlin;\n\n    case 'md':\n    case 'markdown':\n      return Language.markdown;\n\n    case 'py':\n    case 'python':\n      return Language.python;\n\n    case 'tsx':\n    case 'typescriptreact':\n      return Language.typescriptReact;\n\n    case 'ts':\n    case 'typescript':\n      return Language.typescript;\n\n    case 'vue':\n      return Language.vue;\n  }\n\n  return null;\n}\n\n/// A wrapper widget for markdown code blocks that adds a language header\n/// and copy-to-clipboard functionality.\n///\n/// This widget displays a header bar showing the programming language with\n/// an icon and a copy button. When the copy button is pressed, the code\n/// content is copied to the clipboard and the button icon changes to a\n/// checkmark for 1 second to provide visual feedback.\nclass MarkdownPreWrapper extends StatefulWidget {\n  /// The code block content widget to wrap.\n  final Widget child;\n\n  /// The programming language identifier for the code block.\n  final String language;\n\n  /// The raw code text to copy to clipboard.\n  final String code;\n\n  /// Construct a [MarkdownPreWrapper] that wraps a pre block with custom Container\n  const MarkdownPreWrapper(this.child, this.code, this.language, {super.key});\n\n  @override\n  State<MarkdownPreWrapper> createState() => _MarkdownPreWrapperState();\n}\n\nclass _MarkdownPreWrapperState extends State<MarkdownPreWrapper> {\n  bool _isCopied = false;\n  Timer? _timer;\n\n  void copy() async {\n    _timer?.cancel();\n    setState(() => _isCopied = true);\n    await widget.code.copy();\n    _timer = Timer(\n      const Duration(seconds: 1),\n      () => setState(() => _isCopied = false),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.language.isEmpty) {\n      return widget.child;\n    }\n\n    final language = resolveLanguage(widget.language);\n\n    return Container(\n      decoration: BoxDecoration(\n        color: context.colors.surfaceContainerHighest,\n        borderRadius: .circular(8),\n      ),\n      child: Column(\n        children: [\n          Padding(\n            padding: const .only(left: 16),\n            child: Row(\n              spacing: 8,\n              children: [\n                Icon(\n                  language?.icon ?? Symbols.code_rounded,\n                  color: language?.iconColor,\n                  size: 16,\n                ),\n                Expanded(\n                  child: Text(\n                    language?.name ?? widget.language,\n                    style: context.texts.bodyMedium!.copyWith(\n                      fontWeight: .bold,\n                    ),\n                  ),\n                ),\n                IconButton(\n                  onPressed: copy,\n                  tooltip: _isCopied ? '已複製' : '複製',\n                  icon: Icon(\n                    _isCopied ? Symbols.check_rounded : Symbols.content_copy_rounded,\n                    size: 16,\n                  ),\n                  visualDensity: .compact,\n                ),\n              ],\n            ),\n          ),\n          widget.child,\n        ],\n      ),\n    );\n  }\n}\n\n/// Utilities for processing markdown markup formats.\n///\n/// This class provides static methods for:\n/// - HTML entity unescaping\n/// - Auto-linking bare URLs\n/// - Formatting blockquotes\nabstract class MarkdownUtils {\n  static final _escaper = HtmlUnescape();\n  static final _autoLinkPattern = RegExp(\n    r'(?<!<)(?<!\\]\\()(?<!\\]:\\s*)(https?:\\/\\/[^\\s>)\\]]+)(?!>)',\n  );\n\n  /// Unescapes HTML entities in the given text.\n  ///\n  /// Converts HTML entities like `&amp;`, `&lt;`, `&gt;` to their\n  /// corresponding characters.\n  static String unescape(String text) {\n    return _escaper.convert(text);\n  }\n\n  /// Wraps bare URLs in angle brackets to enable auto-linking.\n  ///\n  /// This ensures that URLs not already in markdown link syntax are\n  /// properly recognized and rendered as clickable links.\n  static String fixAutoLinks(String text) {\n    return text.replaceAllMapped(_autoLinkPattern, (m) => '<${m[1]}>');\n  }\n\n  /// Processes text through auto-detection, conversion, and escaping.\n  ///\n  /// This is the main entry point for text processing. It:\n  /// 1. Unescapes HTML entities\n  /// 2. Auto-links bare URLs\n  ///\n  /// Returns the processed markdown string ready for rendering.\n  static String apply(String text) {\n    return fixAutoLinks(\n      unescape(text),\n    );\n  }\n\n  /// Formats text as a markdown blockquote.\n  ///\n  /// Prepends `> ` to each line of the text to create a blockquote block.\n  static String blockquote(String text) {\n    return text.split('\\n').map((line) => '> $line').join('\\n');\n  }\n}\n\n/// Applies JetBrains Mono font to a text style with ligatures enabled.\n///\n/// Used for code blocks and inline code to provide monospace rendering\n/// with programming ligatures enabled via the 'calt' font feature.\nTextStyle applyFont(TextStyle style) {\n  return GoogleFonts.jetBrainsMono(\n    textStyle: style,\n    fontFeatures: [.enable('calt')],\n  );\n}\n\nMap<String, TextStyle> _githubTheme = {\n  'root': applyFont(\n    TextStyle(color: Color(0xff333333), backgroundColor: Color(0xfff8f8f8)),\n  ),\n  'comment': applyFont(\n    TextStyle(color: Color(0xff999988), fontStyle: .italic),\n  ),\n  'quote': applyFont(\n    TextStyle(color: Color(0xff999988), fontStyle: .italic),\n  ),\n  'keyword': applyFont(\n    TextStyle(color: Color(0xff333333), fontWeight: .bold),\n  ),\n  'selector-tag': applyFont(\n    TextStyle(color: Color(0xff333333), fontWeight: .bold),\n  ),\n  'subst': applyFont(\n    TextStyle(color: Color(0xff333333), fontWeight: .normal),\n  ),\n  'number': applyFont(TextStyle(color: Color(0xff008080))),\n  'literal': applyFont(TextStyle(color: Color(0xff008080))),\n  'variable': applyFont(TextStyle(color: Color(0xff008080))),\n  'template-variable': applyFont(TextStyle(color: Color(0xff008080))),\n  'string': applyFont(TextStyle(color: Color(0xffdd1144))),\n  'doctag': applyFont(TextStyle(color: Color(0xffdd1144))),\n  'title': applyFont(\n    TextStyle(color: Color(0xff990000), fontWeight: .bold),\n  ),\n  'section': applyFont(\n    TextStyle(color: Color(0xff990000), fontWeight: .bold),\n  ),\n  'selector-id': applyFont(\n    TextStyle(color: Color(0xff990000), fontWeight: .bold),\n  ),\n  'type': applyFont(\n    TextStyle(color: Color(0xff445588), fontWeight: .bold),\n  ),\n  'tag': applyFont(\n    TextStyle(color: Color(0xff000080), fontWeight: .normal),\n  ),\n  'name': applyFont(\n    TextStyle(color: Color(0xff000080), fontWeight: .normal),\n  ),\n  'attribute': applyFont(\n    TextStyle(color: Color(0xff000080), fontWeight: .normal),\n  ),\n  'regexp': applyFont(TextStyle(color: Color(0xff009926))),\n  'link': applyFont(TextStyle(color: Color(0xff009926))),\n  'symbol': applyFont(TextStyle(color: Color(0xff990073))),\n  'bullet': applyFont(TextStyle(color: Color(0xff990073))),\n  'built_in': applyFont(TextStyle(color: Color(0xff0086b3))),\n  'builtin-name': applyFont(TextStyle(color: Color(0xff0086b3))),\n  'meta': applyFont(\n    TextStyle(color: Color(0xff999999), fontWeight: .bold),\n  ),\n  'deletion': applyFont(TextStyle(backgroundColor: Color(0xffffdddd))),\n  'addition': applyFont(TextStyle(backgroundColor: Color(0xffddffdd))),\n  'emphasis': applyFont(TextStyle(fontStyle: .italic)),\n  'strong': applyFont(TextStyle(fontWeight: .bold)),\n};\n\nMap<String, TextStyle> _githubThemeDark = {\n  'root': applyFont(\n    TextStyle(color: Color(0xffadbac7), backgroundColor: Color(0xff22272e)),\n  ),\n  'comment': applyFont(TextStyle(color: Color(0xff768390))),\n  'quote': applyFont(TextStyle(color: Color(0xff8ddb8c))),\n  'keyword': applyFont(TextStyle(color: Color(0xfff47067))),\n  'selector-tag': applyFont(TextStyle(color: Color(0xfff47067))),\n  'subst': applyFont(TextStyle(color: Color(0xffadbac7))),\n  'number': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'literal': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'variable': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'template-variable': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'string': applyFont(TextStyle(color: Color(0xff96d0ff))),\n  'doctag': applyFont(TextStyle(color: Color(0xff96d0ff))),\n  'title': applyFont(TextStyle(color: Color(0xffdcbdfb))),\n  'section': applyFont(\n    TextStyle(color: Color(0xff316dca), fontWeight: .bold),\n  ),\n  'selector-id': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'type': applyFont(TextStyle(color: Color(0xffdcbdfb))),\n  'tag': applyFont(TextStyle(color: Color(0xff8ddb8c))),\n  'name': applyFont(TextStyle(color: Color(0xff8ddb8c))),\n  'attribute': applyFont(TextStyle(color: Color(0xff6cb6ff))),\n  'regexp': applyFont(TextStyle(color: Color(0xff96d0ff))),\n  'link': applyFont(TextStyle(color: Color(0xff96d0ff))),\n  'symbol': applyFont(TextStyle(color: Color(0xfff69d50))),\n  'bullet': applyFont(TextStyle(color: Color(0xffeac55f))),\n  'built_in': applyFont(TextStyle(color: Color(0xfff69d50))),\n  'builtin-name': applyFont(TextStyle(color: Color(0xfff69d50))),\n  'meta': applyFont(TextStyle(color: Color(0xff768390))),\n  'deletion': applyFont(\n    TextStyle(color: Color(0xffffd8d3), backgroundColor: Color(0xff78191b)),\n  ),\n  'addition': applyFont(\n    TextStyle(color: Color(0xffb4f1b4), backgroundColor: Color(0xff1b4721)),\n  ),\n  'emphasis': applyFont(\n    TextStyle(color: Color(0xffadbac7), fontStyle: .italic),\n  ),\n  'strong': applyFont(\n    TextStyle(color: Color(0xffadbac7), fontWeight: .bold),\n  ),\n};\n\nclass _H1Config extends H1Config {\n  final Color divierColor;\n\n  _H1Config({required this.divierColor, super.style});\n\n  @override\n  HeadingDivider? get divider => HeadingDivider.h1.copy(color: divierColor);\n}\n\nclass _H2Config extends H2Config {\n  final Color divierColor;\n\n  _H2Config({required this.divierColor, super.style});\n\n  @override\n  HeadingDivider? get divider => HeadingDivider.h2.copy(color: divierColor);\n}\n\nclass _H3Config extends H3Config {\n  _H3Config({super.style});\n\n  @override\n  HeadingDivider? get divider => null;\n}\n\nclass Markdown extends StatelessWidget {\n  final String text;\n\n  const Markdown(this.text, {super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final isDark = context.theme.brightness == Brightness.dark;\n\n    TextStyle applyFont(TextStyle style) => GoogleFonts.lato(textStyle: style);\n\n    final headlineSmall = applyFont(context.texts.headlineSmall!);\n    final titleLarge = applyFont(context.texts.titleLarge!);\n    final titleMedium = applyFont(context.texts.titleMedium!);\n    final titleSmall = applyFont(context.texts.titleSmall!);\n    final bodyMedium = applyFont(context.texts.bodyMedium!);\n    final body = applyFont(context.texts.bodyLarge!);\n\n    return MarkdownBlock(\n      data: MarkdownUtils.apply(text),\n      config: (isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig).copy(\n        configs: [\n          _H1Config(\n            style: headlineSmall.copyWith(fontWeight: .bold),\n            divierColor: context.colors.outlineVariant,\n          ),\n          _H2Config(\n            style: titleLarge.copyWith(fontWeight: .bold),\n            divierColor: context.colors.outlineVariant,\n          ),\n          _H3Config(\n            style: titleMedium.copyWith(fontWeight: .bold),\n          ),\n          H4Config(\n            style: titleSmall.copyWith(fontWeight: .bold),\n          ),\n          HrConfig(color: context.colors.outlineVariant),\n          LinkConfig(\n            style: TextStyle(\n              color: context.colors.primary,\n              decoration: .underline,\n              decorationColor: context.colors.primary,\n            ),\n            onTap: (url) => url.launch(),\n          ),\n          ImgConfig(\n            builder: (url, attributes) {\n              return CachedNetworkImage(imageUrl: url);\n            },\n          ),\n          BlockquoteConfig(\n            sideColor: context.colors.outlineVariant,\n            textColor: context.colors.outline,\n          ),\n          PConfig(textStyle: body),\n          CodeConfig(style: applyFont(bodyMedium)),\n          PreConfig(\n            wrapper: MarkdownPreWrapper.new,\n            margin: .zero,\n            decoration: BoxDecoration(\n              color: context.colors.surfaceContainerHigh,\n              borderRadius: BorderRadius.vertical(\n                bottom: Radius.circular(8),\n              ),\n            ),\n            language: 'text',\n            theme: isDark ? _githubThemeDark : _githubTheme,\n            styleNotMatched: applyFont(bodyMedium),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/report/enlargeable_image.dart",
    "content": "import 'package:cached_network_image/cached_network_image.dart';\nimport 'package:dpip/route/image_viewer/image_viewer.dart';\nimport 'package:flutter/material.dart';\n\nclass EnlargeableImage extends StatelessWidget {\n  final double aspectRatio;\n  final String heroTag;\n  final String imageUrl;\n  final String imageName;\n  final VoidCallback? onLoadFailed;\n\n  const EnlargeableImage({\n    super.key,\n    required this.aspectRatio,\n    required this.heroTag,\n    required this.imageUrl,\n    required this.imageName,\n    this.onLoadFailed,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 8),\n      child: AspectRatio(\n        aspectRatio: aspectRatio,\n        child: ClipRRect(\n          borderRadius: BorderRadius.circular(8),\n          child: Stack(\n            children: [\n              Hero(\n                tag: heroTag,\n                child: CachedNetworkImage(\n                  imageUrl: imageUrl,\n                  fit: BoxFit.cover,\n                  errorWidget: (context, url, error) {\n                    onLoadFailed?.call();\n                    return const SizedBox.shrink();\n                  },\n                ),\n              ),\n              Positioned.fill(\n                child: Material(\n                  color: Colors.transparent,\n                  child: InkWell(\n                    onTap: () {\n                      Navigator.push(\n                        context,\n                        MaterialPageRoute(\n                          builder: (context) {\n                            return ImageViewerRoute(\n                              heroTag: heroTag,\n                              imageUrl: imageUrl,\n                              imageName: imageName,\n                            );\n                          },\n                        ),\n                      );\n                    },\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/report/intensity_box.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:flutter/material.dart';\n\nclass IntensityBox extends StatelessWidget {\n  final int intensity;\n  final double size;\n  final double borderRadius;\n  final bool border;\n\n  const IntensityBox({\n    super.key,\n    required this.intensity,\n    this.size = 64,\n    this.borderRadius = 16,\n    this.border = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: size,\n      width: size,\n      decoration: border\n          ? BoxDecoration(\n              borderRadius: BorderRadius.circular(borderRadius),\n              border: Border.all(\n                color: IntensityColor.intensity(intensity),\n                width: 3.0,\n              ),\n            )\n          : BoxDecoration(\n              borderRadius: BorderRadius.circular(borderRadius),\n              color: IntensityColor.intensity(intensity),\n            ),\n      child: Center(\n        child: Text(\n          intensity.asIntensityDisplayLabel,\n          style: TextStyle(\n            color: border ? context.colors.onSurface : IntensityColor.onIntensity(intensity),\n            fontSize: size / 2,\n            fontWeight: FontWeight.bold,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/report/list_item.dart",
    "content": "import 'package:dpip/api/model/report/partial_earthquake_report.dart';\nimport 'package:dpip/route/report/report.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/intensity_color.dart';\nimport 'package:dpip/widgets/report/intensity_box.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nclass ReportListItem extends StatelessWidget {\n  final PartialEarthquakeReport report;\n  final double height;\n  final bool showDate;\n  final bool first;\n  final Function refreshReportList;\n\n  const ReportListItem({\n    super.key,\n    required this.report,\n    required this.refreshReportList,\n    this.showDate = false,\n    this.height = 88,\n    this.first = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      child: InkWell(\n        borderRadius: BorderRadius.circular(24),\n        splashColor: IntensityColor.intensity(\n          report.intensity,\n        ).withValues(alpha: 0.16),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8),\n          child: Row(\n            children: [\n              /**\n               * 時間\n               */\n              SizedBox(\n                width: 88,\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.end,\n                  children: [\n                    if (showDate)\n                      Text(\n                        DateFormat('yyyy/MM/dd').format(report.time),\n                        textAlign: TextAlign.right,\n                        style: TextStyle(\n                          color: context.colors.onSurfaceVariant,\n                          fontSize: 12,\n                          fontWeight: FontWeight.bold,\n                        ),\n                      ),\n                    Text(\n                      DateFormat('HH:mm:ss').format(report.time),\n                      textAlign: TextAlign.right,\n                      style: TextStyle(\n                        color: context.colors.onSurfaceVariant,\n                        fontSize: 12,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 18),\n                child: Stack(\n                  alignment: Alignment.center,\n                  children: [\n                    if (first)\n                      Padding(\n                        padding: EdgeInsets.only(top: height / 2),\n                        child: Container(\n                          width: 2,\n                          height: first ? height / 2 : height,\n                          color: context.colors.outlineVariant, // Color of the vertical line\n                        ),\n                      )\n                    else\n                      Container(\n                        width: 2,\n                        height: first ? height / 2 : height,\n                        color: context.colors.outlineVariant, // Color of the vertical line\n                      ),\n                    IntensityBox(\n                      intensity: report.intensity,\n                      size: 36,\n                      borderRadius: 36,\n                      border: !report.hasNumber,\n                    ),\n                  ],\n                ),\n              ),\n              // Text(report.hasNumber ? report.number! : \"小區域\"),\n              Expanded(\n                child: Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    /**\n                     * 位置\n                     */\n                    Text(\n                      report.extractLocation(),\n                      style: TextStyle(\n                        fontSize: report.hasNumber ? 20 : 18,\n                        fontWeight: report.hasNumber ? FontWeight.bold : FontWeight.normal,\n                      ),\n                    ),\n                    const SizedBox(height: 2),\n                    /**\n                     * 規模、深度\n                     */\n                    Text(\n                      'M ${report.magnitude}　深度 ${report.depth} km',\n                      style: TextStyle(color: context.colors.onSurfaceVariant),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n        onTap: () async {\n          await Navigator.push(\n            context,\n            MaterialPageRoute(\n              builder: (context) {\n                return ReportRoute(id: report.id);\n              },\n            ),\n          );\n          refreshReportList();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/responsive/responsive_container.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/cupertino.dart';\n\nenum ResponsiveMode {\n  content,\n  panel,\n}\n\nclass ResponsiveContainer extends StatelessWidget {\n  final Widget child;\n  final double? maxWidth;\n  final ResponsiveMode mode;\n\n  const ResponsiveContainer({\n    required this.child,\n    this.maxWidth,\n    this.mode = ResponsiveMode.content,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final width = constraints.maxWidth;\n        final isLargeTablet = width >= 800;\n\n        double contentMaxWidth;\n        Alignment alignment;\n\n        switch (mode) {\n          case ResponsiveMode.panel:\n            if (isLargeTablet) {\n              contentMaxWidth = maxWidth ?? width * 0.45;\n            } else {\n              contentMaxWidth = width;\n            }\n            alignment = isLargeTablet ? Alignment.centerLeft : Alignment.center;\n            break;\n\n          case ResponsiveMode.content:\n          default:\n            contentMaxWidth = width >= 600 ? min(width * 0.9, maxWidth ?? 750) : width;\n            alignment = Alignment.center;\n        }\n\n        return Align(\n          alignment: alignment,\n          child: ConstrainedBox(\n            constraints: BoxConstraints(maxWidth: contentMaxWidth),\n            child: child,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/settings/theme/theme_radio_tile.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:material_symbols_icons/symbols.dart';\n\nclass ThemeRadioTile extends StatelessWidget {\n  final ThemeMode value;\n  final ThemeMode groupValue;\n  final void Function()? onTap;\n  final void Function(ThemeMode?)? onChanged;\n  final String title;\n  final ThemeData theme;\n\n  const ThemeRadioTile({\n    super.key,\n    required this.value,\n    required this.groupValue,\n    required this.title,\n    required this.theme,\n    this.onChanged,\n    this.onTap,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: context.colors.surfaceContainerHigh,\n      borderRadius: BorderRadius.circular(16),\n      clipBehavior: Clip.antiAlias,\n      child: InkWell(\n        borderRadius: BorderRadius.circular(16),\n        onTap: onTap,\n        child: Column(\n          children: [\n            Container(\n              height: 96,\n              decoration: BoxDecoration(\n                color: theme.colorScheme.surfaceContainer,\n              ),\n              child: Center(\n                child: Icon(\n                  switch (value) {\n                    ThemeMode.light => Symbols.light_mode,\n                    ThemeMode.dark => Symbols.dark_mode,\n                    ThemeMode.system => Symbols.smartphone,\n                  },\n                  color: theme.colorScheme.onSurface,\n                  size: 32,\n                ),\n              ),\n            ),\n            Row(\n              children: [\n                Radio(\n                  value: value,\n                  groupValue: groupValue,\n                  onChanged: onChanged,\n                  fillColor: WidgetStateProperty.resolveWith<Color>((\n                    Set<WidgetState> states,\n                  ) {\n                    if (states.contains(WidgetState.selected)) {\n                      return context.colors.primary;\n                    }\n                    return context.colors.outline;\n                  }),\n                ),\n                Text(title),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/sheet/bottom_sheet_drag_handle.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/widgets.dart';\n\nclass BottomSheetDragHandle extends StatelessWidget {\n  const BottomSheetDragHandle({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 24,\n      child: Center(\n        child: Container(\n          width: 32,\n          height: 4,\n          decoration: BoxDecoration(\n            color: context.colors.onSurfaceVariant.withValues(alpha: 0.4),\n            borderRadius: BorderRadius.circular(16),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/sheet/morphing_sheet.dart",
    "content": "import 'dart:ui';\n\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/widgets/sheet/morphing_sheet_controller.dart';\nimport 'package:flutter/material.dart';\n\ntypedef MorphingSheetBuilder =\n    Widget Function(\n      BuildContext context,\n      ScrollController controller,\n      MorphingSheetController sheetController,\n    );\n\nclass MorphingSheet extends StatefulWidget {\n  final MorphingSheetBuilder? fullBuilder;\n  final MorphingSheetBuilder partialBuilder;\n  final double maxChildSize;\n  final double fullThreshold;\n  final Color? backgroundColor;\n  final Color? borderColor;\n  final BorderRadius? borderRadius;\n  final double? borderWidth;\n  final EdgeInsets floatingPadding;\n  final double elevation;\n  final String? title;\n  final bool showBackButton;\n  final MorphingSheetController? controller;\n\n  const MorphingSheet({\n    super.key,\n    this.fullBuilder,\n    required this.partialBuilder,\n    this.maxChildSize = 1.0,\n    this.fullThreshold = 0.8,\n    this.backgroundColor,\n    this.borderColor,\n    this.borderRadius,\n    this.borderWidth,\n    this.floatingPadding = const EdgeInsets.symmetric(horizontal: 16.0),\n    this.elevation = 8.0,\n    this.title,\n    this.showBackButton = true,\n    this.controller,\n  });\n\n  @override\n  State<MorphingSheet> createState() => _MorphingSheetState();\n}\n\nclass _MorphingSheetState extends State<MorphingSheet> with SingleTickerProviderStateMixin {\n  late DraggableScrollableController _controller;\n  late AnimationController _morphController;\n  bool _isSnapping = false;\n  final GlobalKey _contentKey = GlobalKey();\n  final GlobalKey _partialKey = GlobalKey();\n  bool _isOverflowing = false;\n  Size? _partialSize;\n\n  static const double _verticalPadding = 8.0;\n  static const double _bottomMargin = 16.0;\n  static const double _minHeightRatio = 0.15;\n  static const double _maxHeightRatio = 0.3;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = DraggableScrollableController();\n    _morphController = AnimationController(\n      vsync: this,\n      duration: MorphingSheetController.enterDuration,\n      reverseDuration: MorphingSheetController.exitDuration,\n    );\n\n    _controller.addListener(_onSheetPositionChanged);\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _measureSizes();\n      _attachController();\n    });\n  }\n\n  @override\n  void didUpdateWidget(MorphingSheet oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.controller != widget.controller) {\n      _detachController(oldWidget.controller);\n      _attachController();\n    }\n  }\n\n  void _attachController() {\n    if (widget.controller != null) {\n      widget.controller!.attach(\n        draggableController: _controller,\n        morphController: _morphController,\n        minChildSize: _minChildSize,\n        maxChildSize: widget.maxChildSize,\n      );\n    }\n  }\n\n  void _detachController(MorphingSheetController? oldController) {\n    if (oldController != null) {\n      oldController.detach();\n    }\n  }\n\n  void _measureSizes() {\n    final RenderBox? contentBox = _contentKey.currentContext?.findRenderObject() as RenderBox?;\n    final RenderBox? partialBox = _partialKey.currentContext?.findRenderObject() as RenderBox?;\n\n    if (contentBox != null) {\n      final screenHeight = context.dimension.height;\n      final isOverflowing = contentBox.size.height > screenHeight * 0.85;\n      if (_isOverflowing != isOverflowing) {\n        setState(() {\n          _isOverflowing = isOverflowing;\n        });\n      }\n    }\n\n    if (partialBox != null) {\n      final newSize = partialBox.size;\n      if (_partialSize != newSize) {\n        setState(() {\n          _partialSize = newSize;\n        });\n      }\n    }\n  }\n\n  double get _minChildSize {\n    if (_partialSize == null) return _minHeightRatio;\n    final screenHeight = context.dimension.height;\n\n    // Calculate total height including all padding and margin\n    final totalHeight =\n        _partialSize!.height + // Content height\n        (_verticalPadding * 2) + // Top and bottom padding\n        _bottomMargin + // Bottom margin\n        context.padding.bottom + // Bottom padding\n        widget.floatingPadding.vertical; // Floating padding\n\n    // Ensure the height ratio is within bounds and responsive to content\n    final calculatedRatio = totalHeight / screenHeight;\n\n    // If the content is taller than the min ratio, use the content height\n    if (calculatedRatio > _minHeightRatio) {\n      return calculatedRatio.clamp(_minHeightRatio, _maxHeightRatio);\n    }\n    return _minHeightRatio;\n  }\n\n  void _onSheetPositionChanged() {\n    if (_isSnapping) return;\n\n    final position = _controller.size;\n    final morphValue = (position - _minChildSize) / (widget.maxChildSize - _minChildSize);\n    _morphController.value = morphValue.clamp(0.0, 1.0);\n  }\n\n  Future<void> _snapToPosition(double targetPosition) async {\n    if (_isSnapping) return;\n    _isSnapping = true;\n\n    try {\n      final isExpanding = targetPosition > _controller.size;\n      final curve = isExpanding ? Easing.emphasizedDecelerate : Easing.emphasizedAccelerate;\n      final duration = isExpanding\n          ? MorphingSheetController.enterDuration\n          : MorphingSheetController.exitDuration;\n\n      await _controller.animateTo(\n        targetPosition,\n        duration: duration,\n        curve: curve,\n      );\n    } finally {\n      _isSnapping = false;\n    }\n  }\n\n  void _onDragEnd(double position) {\n    if (position >= widget.fullThreshold) {\n      _snapToPosition(widget.maxChildSize);\n    } else {\n      _snapToPosition(_minChildSize);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.fullBuilder == null) {\n      final borderRadius = widget.borderRadius?.topLeft.y ?? 16.0;\n\n      return Align(\n        alignment: Alignment.bottomCenter,\n        child: Padding(\n          padding: EdgeInsets.only(\n            left: widget.floatingPadding.horizontal,\n            right: widget.floatingPadding.horizontal,\n            bottom: _verticalPadding,\n          ),\n          child: Container(\n            margin: EdgeInsets.only(\n              bottom: _bottomMargin + context.padding.bottom,\n            ),\n            child: PhysicalModel(\n              color: Colors.transparent,\n              elevation: widget.elevation,\n              shadowColor: context.colors.shadow.withValues(alpha: 0.6),\n              borderRadius: BorderRadius.circular(borderRadius),\n              child: Container(\n                decoration: BoxDecoration(\n                  borderRadius: BorderRadius.circular(borderRadius),\n                  border: Border.all(\n                    width: widget.borderWidth ?? 1,\n                    color: widget.borderColor ?? context.colors.outline.withValues(alpha: 0.2),\n                  ),\n                ),\n                child: ClipRRect(\n                  borderRadius: BorderRadius.circular(borderRadius),\n                  child: BackdropFilter(\n                    filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),\n                    child: Material(\n                      color: (widget.backgroundColor ?? context.colors.surface).withValues(\n                        alpha: 0.6,\n                      ),\n                      borderRadius: BorderRadius.circular(borderRadius),\n                      clipBehavior: Clip.antiAlias,\n                      child: Stack(\n                        children: [\n                          // 漸層\n                          if (_morphController.value < 1.0)\n                            Positioned.fill(\n                              child: DecoratedBox(\n                                decoration: BoxDecoration(\n                                  gradient: LinearGradient(\n                                    begin: Alignment.topLeft,\n                                    end: Alignment.bottomRight,\n                                    colors: [\n                                      context.colors.surfaceTint.withValues(\n                                        alpha: 0.04,\n                                      ),\n                                      context.colors.surfaceTint.withValues(\n                                        alpha: 0.12,\n                                      ),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                            ),\n                          LayoutBuilder(\n                            builder: (context, constraints) {\n                              return SingleChildScrollView(\n                                physics: const NeverScrollableScrollPhysics(),\n                                child: SizedBox(\n                                  key: _partialKey,\n                                  width: constraints.maxWidth,\n                                  child: widget.partialBuilder(\n                                    context,\n                                    ScrollController(),\n                                    widget.controller ?? MorphingSheetController(),\n                                  ),\n                                ),\n                              );\n                            },\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      );\n    }\n\n    return NotificationListener<SizeChangedLayoutNotification>(\n      onNotification: (_) {\n        _measureSizes();\n        return true;\n      },\n      child: SizeChangedLayoutNotifier(\n        child: DraggableScrollableSheet(\n          initialChildSize: _minChildSize,\n          minChildSize: _minChildSize,\n          maxChildSize: widget.maxChildSize,\n          controller: _controller,\n          snap: true,\n          snapSizes: [_minChildSize, widget.maxChildSize],\n          builder: (context, scrollController) {\n            return GestureDetector(\n              onVerticalDragStart: (_) {},\n              onVerticalDragEnd: (_) => _onDragEnd(_controller.size),\n              child: AnimatedBuilder(\n                animation: _morphController,\n                builder: (context, child) {\n                  final horizontalPadding = Tween<double>(\n                    begin: widget.floatingPadding.horizontal,\n                    end: 0.0,\n                  ).transform(_morphController.value);\n\n                  final bottomPadding = Tween<double>(\n                    begin: _verticalPadding,\n                    end: 0.0,\n                  ).transform(_morphController.value);\n\n                  final isFullScreen =\n                      _morphController.value == 1.0 && _controller.size == widget.maxChildSize;\n\n                  final borderRadius = !isFullScreen\n                      ? Tween<double>(\n                          begin: widget.borderRadius?.topLeft.y ?? 16.0,\n                          end: _isOverflowing ? 0.0 : (widget.borderRadius?.topLeft.y ?? 16.0),\n                        ).transform(_morphController.value)\n                      : 0.0;\n\n                  final elevation = Tween<double>(\n                    begin: widget.elevation,\n                    end: _isOverflowing ? 0.0 : widget.elevation,\n                  ).transform(_morphController.value);\n\n                  final marginBottom = Tween<double>(\n                    begin: _bottomMargin + context.padding.bottom,\n                    end: 0.0,\n                  ).transform(_morphController.value);\n\n                  return Padding(\n                    padding: EdgeInsets.only(\n                      left: horizontalPadding,\n                      right: horizontalPadding,\n                      bottom: bottomPadding,\n                    ),\n                    child: Container(\n                      margin: EdgeInsets.only(bottom: marginBottom),\n                      child: PhysicalModel(\n                        color: Colors.transparent,\n                        elevation: elevation,\n                        shadowColor: context.colors.shadow.withValues(\n                          alpha: 0.6,\n                        ),\n                        borderRadius: BorderRadius.circular(borderRadius),\n                        child: Container(\n                          decoration: BoxDecoration(\n                            borderRadius: BorderRadius.circular(borderRadius),\n                            border: Border.all(\n                              color: context.colors.outline.withValues(\n                                alpha: Tween<double>(\n                                  begin: 0.2,\n                                  end: 0.0,\n                                ).transform(_morphController.value),\n                              ),\n                              width: Tween<double>(\n                                begin: 1.0,\n                                end: 0.0,\n                              ).transform(_morphController.value),\n                            ),\n                          ),\n                          child: ClipRRect(\n                            borderRadius: BorderRadius.circular(borderRadius),\n                            child: BackdropFilter(\n                              filter: ImageFilter.blur(\n                                sigmaX: Tween<double>(\n                                  begin: 16,\n                                  end: 0,\n                                ).transform(_morphController.value),\n                                sigmaY: Tween<double>(\n                                  begin: 16,\n                                  end: 0,\n                                ).transform(_morphController.value),\n                              ),\n                              child: Material(\n                                color: (widget.backgroundColor ?? context.colors.surface)\n                                    .withValues(\n                                      alpha: Tween<double>(\n                                        begin: 0.6,\n                                        end: 1.0,\n                                      ).transform(_morphController.value),\n                                    ),\n                                borderRadius: BorderRadius.circular(\n                                  borderRadius,\n                                ),\n                                clipBehavior: Clip.antiAlias,\n                                child: Stack(\n                                  children: [\n                                    // 漸層\n                                    if (_morphController.value < 1.0)\n                                      Positioned.fill(\n                                        child: Opacity(\n                                          opacity: 1.0 - _morphController.value,\n                                          child: DecoratedBox(\n                                            decoration: BoxDecoration(\n                                              gradient: LinearGradient(\n                                                begin: Alignment.topLeft,\n                                                end: Alignment.bottomRight,\n                                                colors: [\n                                                  context.colors.surfaceTint.withValues(\n                                                    alpha: 0.04,\n                                                  ),\n                                                  context.colors.surfaceTint.withValues(\n                                                    alpha: 0.12,\n                                                  ),\n                                                ],\n                                              ),\n                                            ),\n                                          ),\n                                        ),\n                                      ),\n                                    Stack(\n                                      children: [\n                                        Opacity(\n                                          opacity: 1 - _morphController.value,\n                                          child: LayoutBuilder(\n                                            builder: (context, constraints) {\n                                              return SingleChildScrollView(\n                                                physics: const NeverScrollableScrollPhysics(),\n                                                padding: EdgeInsets.zero,\n                                                child: SizedBox(\n                                                  key: _partialKey,\n                                                  width: constraints.maxWidth,\n                                                  child: widget.partialBuilder(\n                                                    context,\n                                                    scrollController,\n                                                    widget.controller ?? MorphingSheetController(),\n                                                  ),\n                                                ),\n                                              );\n                                            },\n                                          ),\n                                        ),\n                                        Opacity(\n                                          opacity: _morphController.value,\n                                          child: _buildFullContent(\n                                            scrollController,\n                                          ),\n                                        ),\n                                      ],\n                                    ),\n                                  ],\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  );\n                },\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFullContent(ScrollController scrollController) {\n    final content = widget.fullBuilder!(\n      context,\n      scrollController,\n      widget.controller ?? MorphingSheetController(),\n    );\n\n    if (!_isOverflowing) {\n      return Container(key: _contentKey, child: content);\n    }\n\n    return Material(color: Colors.transparent, child: content);\n  }\n\n  @override\n  void dispose() {\n    _detachController(widget.controller);\n    _controller.dispose();\n    _morphController.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "lib/widgets/sheet/morphing_sheet_controller.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// A controller for [MorphingSheet] that allows controlling the sheet's state\n/// and animation from outside the widget.\nclass MorphingSheetController {\n  /// The controller for the draggable scrollable sheet\n  DraggableScrollableController? _draggableController;\n\n  /// The controller for the morph animation\n  AnimationController? _morphController;\n\n  /// Whether the sheet is currently snapping to a position\n  bool _isSnapping = false;\n\n  /// The minimum size of the sheet\n  double? _minChildSize;\n\n  /// The maximum size of the sheet\n  double? _maxChildSize;\n\n  /// The duration for enter animations (expanding)\n  static const enterDuration = Duration(milliseconds: 400);\n\n  /// The duration for exit animations (collapsing)\n  static const exitDuration = Duration(milliseconds: 200);\n\n  /// Attaches the controller to a [MorphingSheet]\n  void attach({\n    required DraggableScrollableController draggableController,\n    required AnimationController morphController,\n    required double minChildSize,\n    required double maxChildSize,\n  }) {\n    _draggableController = draggableController;\n    _morphController = morphController;\n    _minChildSize = minChildSize;\n    _maxChildSize = maxChildSize;\n  }\n\n  /// Detaches the controller from its [MorphingSheet]\n  void detach() {\n    _draggableController = null;\n    _morphController = null;\n    _minChildSize = null;\n    _maxChildSize = null;\n    _isSnapping = false;\n  }\n\n  /// Returns true if the controller is attached to a [MorphingSheet]\n  bool get isAttached => _draggableController != null;\n\n  /// Returns the current size of the sheet, between [minChildSize] and [maxChildSize]\n  double get size => _draggableController?.size ?? _minChildSize ?? 0.0;\n\n  /// Returns the current morph value, between 0.0 and 1.0\n  double get morphValue => _morphController?.value ?? 0.0;\n\n  /// Returns true if the sheet is currently expanded to its maximum size\n  bool get isExpanded => size >= (_maxChildSize ?? 1.0);\n\n  /// Returns true if the sheet is currently at its minimum size\n  bool get isCollapsed => size <= (_minChildSize ?? 0.0);\n\n  /// Expands the sheet to its maximum size with an animation\n  Future<void> expand() async {\n    if (!isAttached || _isSnapping) return;\n    await snapToSize(_maxChildSize!);\n  }\n\n  /// Collapses the sheet to its minimum size with an animation\n  Future<void> collapse() async {\n    if (!isAttached || _isSnapping) return;\n    await snapToSize(_minChildSize!);\n  }\n\n  /// Snaps the sheet to a specific size with an animation\n  Future<void> snapToSize(double targetSize) async {\n    if (!isAttached || _isSnapping) return;\n    _isSnapping = true;\n\n    try {\n      final isExpanding = targetSize > size;\n      final curve = isExpanding ? Easing.emphasizedDecelerate : Easing.emphasizedAccelerate;\n      final duration = isExpanding ? enterDuration : exitDuration;\n\n      await _draggableController!.animateTo(\n        targetSize,\n        duration: duration,\n        curve: curve,\n      );\n    } finally {\n      _isSnapping = false;\n    }\n  }\n\n  /// Disposes the controller\n  void dispose() {\n    detach();\n  }\n}\n"
  },
  {
    "path": "lib/widgets/sheet/sheet_container.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass SheetContainer extends StatelessWidget {\n  final IconData? icon;\n  final Widget title;\n  final Widget? description;\n  final Widget child;\n  final ScrollController? scrollController;\n  final Color? color;\n\n  const SheetContainer({\n    super.key,\n    required this.child,\n    required this.title,\n    this.description,\n    this.icon,\n    this.scrollController,\n    this.color,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SafeArea(\n      child: Material(\n        color: color ?? Colors.transparent,\n        borderRadius: const .vertical(top: .circular(16)),\n        child: SingleChildScrollView(\n          controller: scrollController,\n          padding: const .symmetric(horizontal: 20, vertical: 8),\n          child: Column(\n            mainAxisSize: .min,\n            crossAxisAlignment: .start,\n            spacing: 8,\n            children: [\n              Padding(\n                padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),\n                child: Column(\n                  crossAxisAlignment: .start,\n                  spacing: 16,\n                  children: [\n                    Row(\n                      spacing: 8,\n                      children: [\n                        if (icon != null) Icon(icon, size: 28),\n                        DefaultTextStyle(\n                          style: context.texts.titleLarge!,\n                          child: title,\n                        ),\n                      ],\n                    ),\n                    if (description != null)\n                      DefaultTextStyle(\n                        child: description!,\n                        style: context.texts.bodyMedium!.copyWith(\n                          color: context.colors.onSurfaceVariant,\n                        ),\n                      ),\n                  ],\n                ),\n              ),\n              child,\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/shell_wrapper.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass ShellWrapper extends StatelessWidget {\n  final Widget child;\n\n  const ShellWrapper(this.child, {super.key});\n\n  bool _canPop(BuildContext context) {\n    final lastMatch = context.router.routerDelegate.currentConfiguration.matches.lastOrNull;\n\n    if (lastMatch is ShellRouteMatch) {\n      return lastMatch.matches.length == 1;\n    }\n\n    return true;\n  }\n\n  @override\n  Widget build(BuildContext context) => PopScope(canPop: _canPop(context), child: child);\n}\n"
  },
  {
    "path": "lib/widgets/thunderstorm_shader_background.dart",
    "content": "import 'dart:ui' as ui;\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass ThunderstormShaderBackground extends StatefulWidget {\n  final Widget? child;\n  final bool animated;\n  final String imagePath;\n  final double lightningIntensity;\n  final double rainAmount;\n\n  const ThunderstormShaderBackground({\n    super.key,\n    this.child,\n    this.animated = true,\n    this.imagePath = 'assets/wallpaper/dusk/vending_machine.jpg',\n    this.lightningIntensity = 1,\n    this.rainAmount = 0.3,\n  });\n\n  @override\n  State<ThunderstormShaderBackground> createState() => _ThunderstormShaderBackgroundState();\n}\n\nclass _ThunderstormShaderBackgroundState extends State<ThunderstormShaderBackground>\n    with SingleTickerProviderStateMixin {\n  ui.FragmentShader? _shader;\n  ui.Image? _image;\n  late final AnimationController _controller;\n  int _startTime = 0;\n\n  double get _elapsedTime => (DateTime.now().millisecondsSinceEpoch - _startTime) / 1000.0;\n\n  @override\n  void initState() {\n    super.initState();\n    _startTime = DateTime.now().millisecondsSinceEpoch;\n\n    _controller = AnimationController(\n      duration: const Duration(seconds: 1),\n      vsync: this,\n    );\n\n    if (widget.animated) {\n      _controller.repeat();\n    }\n\n    _loadAssets();\n  }\n\n  Future<void> _loadAssets() async {\n    try {\n      final results = await Future.wait([\n        ui.FragmentProgram.fromAsset('shaders/thunderstorm.frag'),\n        _loadImage(widget.imagePath),\n      ]);\n\n      if (mounted) {\n        setState(() {\n          _shader = (results[0] as ui.FragmentProgram).fragmentShader();\n          _image = results[1] as ui.Image;\n        });\n      }\n    } catch (e) {\n      debugPrint('Failed to load thunderstorm shader: $e');\n    }\n  }\n\n  Future<ui.Image> _loadImage(String path) async {\n    final data = await rootBundle.load(path);\n    final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());\n    final frame = await codec.getNextFrame();\n    return frame.image;\n  }\n\n  @override\n  void didUpdateWidget(ThunderstormShaderBackground oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (widget.animated != oldWidget.animated) {\n      widget.animated ? _controller.repeat() : _controller.stop();\n    }\n\n    if (widget.imagePath != oldWidget.imagePath) {\n      _image?.dispose();\n      _image = null;\n      _loadAssets();\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    _shader?.dispose();\n    _image?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_shader == null || _image == null) {\n      return Container(\n        color: const Color(0xFF1a1a2e),\n        child: widget.child,\n      );\n    }\n\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final size = Size(constraints.maxWidth, constraints.maxHeight);\n\n        return AnimatedBuilder(\n          animation: _controller,\n          builder: (context, _) {\n            return CustomPaint(\n              size: size,\n              painter: _ThunderstormShaderPainter(\n                shader: _shader!,\n                image: _image!,\n                time: _elapsedTime,\n                lightningIntensity: widget.lightningIntensity,\n                rainAmount: widget.rainAmount,\n              ),\n              child: widget.child,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _ThunderstormShaderPainter extends CustomPainter {\n  final ui.FragmentShader shader;\n  final ui.Image image;\n  final double time;\n  final double lightningIntensity;\n  final double rainAmount;\n\n  _ThunderstormShaderPainter({\n    required this.shader,\n    required this.image,\n    required this.time,\n    required this.lightningIntensity,\n    required this.rainAmount,\n  });\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    int idx = 0;\n    shader.setFloat(idx++, time);\n    shader.setFloat(idx++, size.width);\n    shader.setFloat(idx++, size.height);\n    shader.setFloat(idx++, lightningIntensity);\n    shader.setFloat(idx++, rainAmount);\n    shader.setImageSampler(0, image);\n\n    final paint = Paint()..shader = shader;\n    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);\n  }\n\n  @override\n  bool shouldRepaint(covariant _ThunderstormShaderPainter oldDelegate) {\n    return oldDelegate.time != time ||\n        oldDelegate.lightningIntensity != lightningIntensity ||\n        oldDelegate.rainAmount != rainAmount ||\n        oldDelegate.image != image;\n  }\n}\n"
  },
  {
    "path": "lib/widgets/typography.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// A widget that displays text using Material Design 3 display text styles.\n///\n/// Display text is the largest text style in Material Design 3, typically used for hero text and prominent displays.\n/// This widget provides convenient constructors for small, medium, and large display text variants, each corresponding\n/// to Material Design 3's display text styles.\n///\n/// The text style is automatically retrieved from the theme's [TextTheme] and can be customized through optional\n/// parameters like [color], [weight], and [style].\nclass DisplayText extends StatelessWidget {\n  /// The text content to display.\n  final String data;\n\n  /// Optional custom text style that will be merged with the theme's display text style.\n  final TextStyle? style;\n\n  /// Optional text alignment.\n  final TextAlign? align;\n\n  /// Internal function that retrieves the appropriate display text style from the theme.\n  final TextStyle? Function(BuildContext) _textStyleGetter;\n\n  /// Creates a small display text widget.\n  ///\n  /// Uses the theme's `displaySmall` text style. The [color], [weight], and [style] parameters can be used to customize\n  /// the appearance, and will be merged with the theme's base style.\n  DisplayText.small(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.displaySmall);\n\n  /// Creates a medium display text widget.\n  ///\n  /// Uses the theme's `displayMedium` text style. The [color], [weight], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  DisplayText.medium(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.displayMedium);\n\n  /// Creates a large display text widget.\n  ///\n  /// Uses the theme's `displayLarge` text style. The [color], [weight], and [style] parameters can be used to customize\n  /// the appearance, and will be merged with the theme's base style.\n  DisplayText.large(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.displayLarge);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      data,\n      style: _textStyleGetter(context)?.merge(style),\n      textAlign: align,\n    );\n  }\n}\n\n/// A widget that displays text using Material Design 3 headline text styles.\n///\n/// Headline text is used for section headings and prominent titles. This widget provides convenient constructors for\n/// small, medium, and large headline text variants, each corresponding to Material Design 3's headline text styles.\n///\n/// The text style is automatically retrieved from the theme's [TextTheme] and can be customized through optional\n/// parameters like [color], [weight], and [style].\nclass HeadLineText extends StatelessWidget {\n  /// The text content to display.\n  final String data;\n\n  /// Optional custom text style that will be merged with the theme's headline text style.\n  final TextStyle? style;\n\n  /// Optional text alignment.\n  final TextAlign? align;\n\n  /// Internal function that retrieves the appropriate headline text style from the theme.\n  final TextStyle? Function(BuildContext) _textStyleGetter;\n\n  /// Creates a small headline text widget.\n  ///\n  /// Uses the theme's `headlineSmall` text style. The [color], [weight], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  HeadLineText.small(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.headlineSmall);\n\n  /// Creates a medium headline text widget.\n  ///\n  /// Uses the theme's `headlineMedium` text style. The [color], [weight], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  HeadLineText.medium(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.headlineMedium);\n\n  /// Creates a large headline text widget.\n  ///\n  /// Uses the theme's `headlineLarge` text style. The [color], [weight], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  HeadLineText.large(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(color: color, fontWeight: weight).merge(style),\n       _textStyleGetter = ((context) => context.texts.headlineLarge);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      data,\n      style: _textStyleGetter(context)?.merge(style),\n      textAlign: align,\n    );\n  }\n}\n\n/// A widget that displays text using Material Design 3 title text styles.\n///\n/// Title text is used for card titles, list item titles, and other prominent text elements. This widget provides\n/// convenient constructors for small, medium, and large title text variants, each corresponding to Material Design 3's\n/// title text styles.\n///\n/// The text style is automatically retrieved from the theme's [TextTheme] and can be customized through optional\n/// parameters like [color], [weight], [leading], and [style].\nclass TitleText extends StatelessWidget {\n  /// The text content to display.\n  final String data;\n\n  /// Optional custom text style that will be merged with the theme's title text style.\n  final TextStyle? style;\n\n  /// Optional text alignment.\n  final TextAlign? align;\n\n  /// Internal function that retrieves the appropriate title text style from the theme.\n  final TextStyle? Function(BuildContext) _textStyleGetter;\n\n  /// Creates a small title text widget.\n  ///\n  /// Uses the theme's `titleSmall` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  TitleText.small(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.titleSmall);\n\n  /// Creates a medium title text widget.\n  ///\n  /// Uses the theme's `titleMedium` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  TitleText.medium(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.titleMedium);\n\n  /// Creates a large title text widget.\n  ///\n  /// Uses the theme's `titleLarge` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  TitleText.large(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.titleLarge);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      data,\n      style: _textStyleGetter(context)?.merge(style),\n      textAlign: align,\n    );\n  }\n}\n\n/// A widget that displays text using Material Design 3 body text styles.\n///\n/// Body text is used for the main content of the application, such as paragraphs, descriptions, and general text\n/// content. This widget provides convenient constructors for small, medium, and large body text variants, each\n/// corresponding to Material Design 3's body text styles.\n///\n/// The text style is automatically retrieved from the theme's [TextTheme] and can be customized through optional\n/// parameters like [color], [weight], [leading], and [style].\nclass BodyText extends StatelessWidget {\n  /// The text content to display.\n  final String data;\n\n  /// Optional custom text style that will be merged with the theme's body text style.\n  final TextStyle? style;\n\n  /// Optional text alignment.\n  final TextAlign? align;\n\n  /// Internal function that retrieves the appropriate body text style from the theme.\n  final TextStyle? Function(BuildContext) _textStyleGetter;\n\n  /// Creates a small body text widget.\n  ///\n  /// Uses the theme's `bodySmall` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  BodyText.small(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.bodySmall);\n\n  /// Creates a medium body text widget.\n  ///\n  /// Uses the theme's `bodyMedium` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  BodyText.medium(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.bodyMedium);\n\n  /// Creates a large body text widget.\n  ///\n  /// Uses the theme's `bodyLarge` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  BodyText.large(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.bodyLarge);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      data,\n      style: _textStyleGetter(context)?.merge(style),\n      textAlign: align,\n    );\n  }\n}\n\n/// A widget that displays text using Material Design 3 label text styles.\n///\n/// Label text is used for buttons, tabs, and other UI elements that need compact, readable text. This widget provides\n/// convenient constructors for small, medium, and large label text variants, each corresponding to Material Design 3's\n/// label text styles.\n///\n/// The text style is automatically retrieved from the theme's [TextTheme] and can be customized through optional\n/// parameters like [color], [weight], [leading], and [style].\nclass LabelText extends StatelessWidget {\n  /// The text content to display.\n  final String data;\n\n  /// Optional custom text style that will be merged with the theme's label text style.\n  final TextStyle? style;\n\n  /// Optional text alignment.\n  final TextAlign? align;\n\n  /// Internal function that retrieves the appropriate label text style from the theme.\n  final TextStyle? Function(BuildContext) _textStyleGetter;\n\n  /// Creates a small label text widget.\n  ///\n  /// Uses the theme's `labelSmall` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  LabelText.small(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.labelSmall);\n\n  /// Creates a medium label text widget.\n  ///\n  /// Uses the theme's `labelMedium` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  LabelText.medium(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.labelMedium);\n\n  /// Creates a large label text widget.\n  ///\n  /// Uses the theme's `labelLarge` text style. The [color], [weight], [leading], and [style] parameters can be used to\n  /// customize the appearance, and will be merged with the theme's base style.\n  LabelText.large(\n    this.data, {\n    super.key,\n    Color? color,\n    FontWeight? weight,\n    double? leading,\n    TextStyle? style,\n    this.align,\n  }) : style = TextStyle(\n         color: color,\n         fontWeight: weight,\n         height: leading,\n       ).merge(style),\n       _textStyleGetter = ((context) => context.texts.labelLarge);\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      data,\n      style: _textStyleGetter(context)?.merge(style),\n      textAlign: align,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/ui/color_picker.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:dpip/core/i18n.dart';\nimport 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:dpip/utils/extensions/number.dart';\nimport 'package:dpip/utils/extensions/string.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nextension ColorExtension on Color {\n  String toHexString([bool includeAlpha = false]) {\n    final hex = toARGB32().toRadixString(16).toUpperCase().padLeft(8, '0');\n\n    return includeAlpha ? hex.substring(0, 8) : hex.substring(2, 8);\n  }\n}\n\nclass ColorPickerBackgroundPainter extends CustomPainter {\n  final double value;\n\n  const ColorPickerBackgroundPainter({this.value = 1})\n    : assert(value >= 0 && value <= 1, 'Value must be between 0 and 1');\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final colors = List.generate(\n      360,\n      (index) => HSVColor.fromAHSV(1, index.asDouble, 1, value).toColor(),\n    );\n\n    final center = size.center(Offset.zero);\n    final radius = min(size.width, size.height) / 2;\n    final rect = Rect.fromCircle(center: center, radius: radius);\n\n    final sweepPaint = Paint()\n      ..shader = SweepGradient(\n        colors: colors,\n        transform: GradientRotation(-pi / 2),\n      ).createShader(rect);\n\n    canvas.drawCircle(center, radius, sweepPaint);\n\n    final radialPaint = Paint()\n      ..shader = RadialGradient(\n        colors: [\n          HSVColor.fromAHSV(1, 0, 0, value).toColor(),\n          HSVColor.fromAHSV(0, 0, 0, value).toColor(),\n        ],\n      ).createShader(rect);\n\n    canvas.drawCircle(center, radius, radialPaint);\n  }\n\n  @override\n  bool shouldRepaint(covariant ColorPickerBackgroundPainter oldDelegate) =>\n      value != oldDelegate.value;\n}\n\nclass ColorPicker extends StatefulWidget {\n  final HSVColor color;\n  final Size thumbSize;\n  final void Function(HSVColor color)? onChanged;\n\n  const ColorPicker({\n    super.key,\n    this.color = const .fromAHSV(1, 0, 0, 1),\n    this.thumbSize = const Size(24, 24),\n    this.onChanged,\n  });\n\n  @override\n  State<ColorPicker> createState() => _ColorPickerState();\n}\n\nclass _ColorPickerState extends State<ColorPicker> {\n  final _h = TextEditingController();\n  final _s = TextEditingController();\n  final _v = TextEditingController();\n  final _hex = TextEditingController();\n\n  HSVColor get hsv => widget.color;\n\n  Color _ColorFromAHSV(num alpha, num hue, num saturation, num value) => HSVColor.fromAHSV(\n    alpha.asDouble,\n    hue.asDouble,\n    saturation.asDouble,\n    value.asDouble,\n  ).toColor();\n\n  Offset _calculatePositionFromColor(HSVColor color, Size size) {\n    final center = size.center(.zero);\n    final radius = min(size.width, size.height) / 2;\n\n    final angle = (color.hue - 90) * pi / 180;\n\n    final r = color.saturation * radius;\n\n    final dx = r * cos(angle);\n    final dy = r * sin(angle);\n\n    return Offset(center.dx + dx, center.dy + dy);\n  }\n\n  void _updateFromPosition(Offset position, Size size) {\n    final center = size.center(.zero);\n    final radius = min(size.width, size.height) / 2;\n\n    final dx = position.dx - center.dx;\n    final dy = position.dy - center.dy;\n    final distance = sqrt(dx * dx + dy * dy);\n\n    final clampedDistance = distance > radius ? radius : distance;\n\n    double hue = (atan2(dy, dx) * 180 / pi + 90) % 360;\n    if (hue < 0) hue += 360;\n\n    final saturation = clampedDistance / radius;\n\n    _update(hue: hue, saturation: saturation);\n  }\n\n  void _update({num? alpha, num? hue, num? saturation, num? value}) {\n    final onChanged = this.widget.onChanged;\n    if (onChanged == null) return;\n\n    final a = switch (alpha) {\n      null => hsv.alpha,\n      double v => v,\n      num v => v.asDouble,\n    };\n\n    final h = switch (hue) {\n      null => hsv.hue,\n      double v => v,\n      num v => v.asDouble,\n    };\n\n    final s = switch (saturation) {\n      null => hsv.saturation,\n      double v => v,\n      num v => v.asDouble,\n    };\n\n    final v = switch (value) {\n      null => hsv.value,\n      double v => v,\n      num v => v.asDouble,\n    };\n\n    final newColor = HSVColor.fromAHSV(a, h, s, v);\n\n    if (newColor == hsv) return;\n\n    onChanged(newColor);\n  }\n\n  _updateTextControllers(HSVColor color) {\n    _h.value = _h.value.replaced(\n      TextRange(start: 0, end: _h.value.text.length),\n      color.hue.precisionString(2),\n    );\n    _s.value = _s.value.replaced(\n      TextRange(start: 0, end: _s.value.text.length),\n      color.saturation.precisionString(2),\n    );\n    _v.value = _v.value.replaced(\n      TextRange(start: 0, end: _v.value.text.length),\n      color.value.precisionString(2),\n    );\n    _hex.value = _hex.value.replaced(\n      TextRange(start: 0, end: _hex.value.text.length),\n      color.toColor().toHexString(),\n    );\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _updateTextControllers(hsv);\n  }\n\n  @override\n  void didUpdateWidget(covariant ColorPicker oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.color != hsv) {\n      _updateTextControllers(hsv);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: .min,\n      crossAxisAlignment: .start,\n      spacing: 8,\n      children: [\n        ClipOval(\n          child: Padding(\n            padding: const .fromLTRB(24, 24, 24, 24),\n            child: LayoutBuilder(\n              builder: (context, constraints) {\n                final radius = min(constraints.maxWidth, constraints.maxHeight);\n                final size = Size.square(radius);\n\n                final thumbPosition = _calculatePositionFromColor(\n                  hsv,\n                  size,\n                );\n\n                return SizedBox.fromSize(\n                  size: size,\n                  child: AspectRatio(\n                    aspectRatio: 1,\n                    child: Stack(\n                      clipBehavior: .none,\n                      children: [\n                        GestureDetector(\n                          behavior: .opaque,\n                          onPanDown: (details) => _updateFromPosition(details.localPosition, size),\n                          onPanUpdate: (details) =>\n                              _updateFromPosition(details.localPosition, size),\n                          child: CustomPaint(\n                            size: size,\n                            painter: ColorPickerBackgroundPainter(\n                              value: hsv.value,\n                            ),\n                          ),\n                        ),\n                        Positioned(\n                          left: thumbPosition.dx - widget.thumbSize.width / 2,\n                          top: thumbPosition.dy - widget.thumbSize.height / 2,\n                          child: Container(\n                            height: widget.thumbSize.height,\n                            width: widget.thumbSize.width,\n                            decoration: BoxDecoration(\n                              shape: .circle,\n                              color: hsv.toColor(),\n                              border: .all(color: Colors.white, width: 3),\n                              boxShadow: kElevationToShadow[2],\n                            ),\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                );\n              },\n            ),\n          ),\n        ),\n        // Hue Slider\n        Stack(\n          alignment: .center,\n          children: [\n            Positioned(\n              height: 16,\n              left: 24,\n              right: 24,\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  borderRadius: .circular(8),\n                  gradient: LinearGradient(\n                    colors: List.generate(\n                      360,\n                      (index) => _ColorFromAHSV(1, index, 1, 1),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            Slider(\n              min: 0,\n              max: 1,\n              value: hsv.hue / 360,\n              label: '${hsv.hue.asInt}°',\n              inactiveColor: Colors.transparent,\n              activeColor: Colors.transparent,\n              thumbColor: context.colors.primary,\n              onChanged: (value) => _update(hue: value * 360),\n            ),\n          ],\n        ),\n        // Saturation Slider\n        Stack(\n          alignment: .center,\n          children: [\n            Positioned(\n              height: 16,\n              left: 24,\n              right: 24,\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  borderRadius: .circular(8),\n                  gradient: LinearGradient(\n                    colors: [\n                      _ColorFromAHSV(1, hsv.hue, 0, hsv.value),\n                      _ColorFromAHSV(1, hsv.hue, 1, hsv.value),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n            Slider(\n              min: 0,\n              max: 1,\n              value: hsv.saturation,\n              label: '${hsv.saturation.asPercentage}%',\n              inactiveColor: Colors.transparent,\n              activeColor: Colors.transparent,\n              thumbColor: context.colors.primary,\n              onChanged: (value) => _update(saturation: value),\n            ),\n          ],\n        ),\n        // Value Slider\n        Stack(\n          alignment: .center,\n          children: [\n            Positioned(\n              height: 16,\n              left: 24,\n              right: 24,\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  borderRadius: .circular(8),\n                  gradient: LinearGradient(\n                    colors: [\n                      _ColorFromAHSV(1, hsv.hue, hsv.saturation, 0),\n                      _ColorFromAHSV(1, hsv.hue, hsv.saturation, 1),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n            Slider(\n              min: 0,\n              max: 1,\n              value: hsv.value,\n              label: '${hsv.value.asPercentage}%',\n              inactiveColor: Colors.transparent,\n              activeColor: Colors.transparent,\n              thumbColor: context.colors.primary,\n              onChanged: (value) => _update(value: value),\n            ),\n          ],\n        ),\n        // Hue Input\n        Padding(\n          padding: const .symmetric(horizontal: 24),\n          child: Row(\n            spacing: 8,\n            children: [\n              Expanded(\n                child: TextFormField(\n                  controller: _h,\n                  decoration: InputDecoration(\n                    border: OutlineInputBorder(borderRadius: .circular(8)),\n                    visualDensity: .compact,\n                    labelText: '色相'.i18n,\n                    suffixText: '°',\n                  ),\n                  keyboardType: .numberWithOptions(decimal: true),\n                  inputFormatters: [\n                    ClampedTextInputFormatter(min: 0, max: 360),\n                  ],\n                  onChanged: (value) => _update(hue: .tryParse(value)),\n                ),\n              ),\n              Expanded(\n                child: TextFormField(\n                  controller: _s,\n                  decoration: InputDecoration(\n                    border: OutlineInputBorder(borderRadius: .circular(8)),\n                    visualDensity: .compact,\n                    labelText: '彩度'.i18n,\n                  ),\n                  keyboardType: .numberWithOptions(decimal: true),\n                  inputFormatters: [\n                    FilteringTextInputFormatter.allow(RegExp(r'^[0-9.]+$')),\n                    ClampedTextInputFormatter(min: 0, max: 1),\n                  ],\n                  onChanged: (value) => _update(saturation: .tryParse(value)),\n                ),\n              ),\n              Expanded(\n                child: TextFormField(\n                  controller: _v,\n                  decoration: InputDecoration(\n                    border: OutlineInputBorder(borderRadius: .circular(8)),\n                    visualDensity: .compact,\n                    labelText: '明度'.i18n,\n                  ),\n                  keyboardType: .numberWithOptions(decimal: true),\n                  inputFormatters: [\n                    FilteringTextInputFormatter.allow(RegExp(r'^[0-9.]+$')),\n                    ClampedTextInputFormatter(min: 0, max: 1),\n                  ],\n                  onChanged: (value) => _update(value: .tryParse(value)),\n                ),\n              ),\n            ],\n          ),\n        ),\n        Padding(\n          padding: const .symmetric(horizontal: 24, vertical: 8),\n          child: TextFormField(\n            controller: _hex,\n            decoration: InputDecoration(\n              border: OutlineInputBorder(borderRadius: .circular(8)),\n              visualDensity: .compact,\n              labelText: '十六進位值'.i18n,\n              prefixText: '#',\n            ),\n            keyboardType: .visiblePassword,\n            inputFormatters: [\n              FilteringTextInputFormatter.allow(RegExp(r'^[0-9A-Fa-f]+$')),\n            ],\n            onChanged: (value) {\n              if (value.isEmpty) return;\n              if (value.length != 6) return;\n\n              final hex = int.parse(value, radix: 16);\n              if (hex < 0 || hex > 0xffffff) return;\n\n              widget.onChanged?.call(\n                HSVColor.fromColor(\n                  Color(.parse('FF$value', radix: 16)),\n                ),\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass ClampedTextInputFormatter extends TextInputFormatter {\n  final double min;\n  final double max;\n\n  const ClampedTextInputFormatter({this.min = 0, this.max = 1});\n\n  @override\n  TextEditingValue formatEditUpdate(\n    TextEditingValue oldValue,\n    TextEditingValue newValue,\n  ) {\n    if (newValue.text.isEmpty) return oldValue;\n    if (newValue.text.asDouble == oldValue.text.asDouble) return newValue;\n\n    return newValue.copyWith(\n      text: clampDouble(\n        newValue.text.asDouble,\n        min,\n        max,\n      ).precision(2).toString(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/ui/icon_container.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\n/// An icon widget with a rounded background container.\n///\n/// This widget wraps an [Icon] in a container with padding and rounded corners,\n/// providing a contained appearance commonly used for prominent icons in modern\n/// UI designs. The background color defaults to a semi-transparent version of\n/// the icon color (16% opacity), but can be customized with a solid color or\n/// gradient.\n///\n/// The container has 8 pixels of padding and a 12 pixel border radius. All\n/// standard [Icon] properties are supported and passed through to the inner icon.\n///\n/// Example:\n/// ```dart\n/// ContainedIcon(\n///   Icons.notifications,\n///   color: Colors.blue,\n/// )\n///\n/// ContainedIcon(\n///   Icons.star,\n///   backgroundColor: Colors.amber,\n///   color: Colors.white,\n///   size: 32,\n/// )\n///\n/// ContainedIcon(\n///   Icons.favorite,\n///   backgroundGradient: LinearGradient(\n///     colors: [Colors.pink, Colors.red],\n///   ),\n///   color: Colors.white,\n/// )\n/// ```\nclass ContainedIcon extends StatelessWidget {\n  /// The icon to display.\n  final IconData icon;\n\n  /// The size of the icon in logical pixels.\n  ///\n  /// Defaults to 24.0.\n  final double? size;\n\n  /// The fill value for the icon.\n  ///\n  /// This is used with Material Symbols icons to control the fill amount.\n  final double? fill;\n\n  /// The stroke weight of the icon.\n  ///\n  /// Defaults to 600.\n  final double? weight;\n\n  /// The grade of the icon.\n  ///\n  /// This is used with Material Symbols icons to control the visual weight.\n  final double? grade;\n\n  /// The optical size of the icon.\n  ///\n  /// This is used with Material Symbols icons for optical corrections.\n  final double? opticalSize;\n\n  /// The padding of the outer Container that contains the Icon.\n  ///\n  /// Defaults to `EdgeInsets.all(8)`\n  final EdgeInsetsGeometry? padding;\n\n  /// The margin of the outer Container that contains the Icon.\n  final EdgeInsetsGeometry? margin;\n\n  /// The color of the icon.\n  ///\n  /// Defaults to the theme's onSurface color if not specified.\n  final Color? color;\n\n  /// A list of shadows to apply to the icon.\n  final List<Shadow>? shadows;\n\n  /// The semantic label for the icon.\n  ///\n  /// Used for accessibility to describe the icon to screen readers.\n  final String? semanticLabel;\n\n  /// The text direction to use for rendering the icon.\n  final TextDirection? textDirection;\n\n  /// Whether to apply text scaling to the icon size.\n  final bool? applyTextScaling;\n\n  /// The blend mode to apply when drawing the icon.\n  final BlendMode? blendMode;\n\n  /// The font weight to use when rendering the icon.\n  final FontWeight? fontWeight;\n\n  /// The background color of the container.\n  ///\n  /// When null and [backgroundGradient] is also null, defaults to the icon\n  /// color with 16% opacity. Ignored if [backgroundGradient] is provided.\n  final Color? backgroundColor;\n\n  /// The background gradient of the container.\n  ///\n  /// When provided, this takes precedence over [backgroundColor].\n  final Gradient? backgroundGradient;\n\n  /// Creates a contained icon with a rounded background.\n  const ContainedIcon(\n    this.icon, {\n    super.key,\n    this.size = 24,\n    this.weight = 600,\n    this.fill,\n    this.grade,\n    this.opticalSize,\n    this.padding,\n    this.margin,\n    this.color,\n    this.shadows,\n    this.semanticLabel,\n    this.textDirection,\n    this.applyTextScaling,\n    this.blendMode,\n    this.fontWeight,\n    this.backgroundColor,\n    this.backgroundGradient,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final color = this.color ?? context.colors.onSurface;\n\n    return Container(\n      padding: padding ?? const .all(8),\n      margin: margin,\n      decoration: BoxDecoration(\n        color: backgroundGradient == null ? backgroundColor ?? color.withValues(alpha: .16) : null,\n        gradient: backgroundGradient,\n        borderRadius: .circular((size ?? 24) * 0.5),\n      ),\n      child: Icon(\n        icon,\n        size: size,\n        fill: fill,\n        weight: weight,\n        grade: grade,\n        opticalSize: opticalSize,\n        color: color,\n        shadows: shadows,\n        semanticLabel: semanticLabel,\n        textDirection: textDirection,\n        applyTextScaling: applyTextScaling,\n        blendMode: blendMode,\n        fontWeight: fontWeight,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/ui/labeled_divider.dart",
    "content": "import 'package:dpip/utils/extensions/build_context.dart';\nimport 'package:flutter/material.dart';\n\nclass LabeledDivider extends StatelessWidget {\n  final String label;\n\n  const LabeledDivider({super.key, required this.label});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 4),\n      child: Row(\n        spacing: 8,\n        children: [\n          const SizedBox(width: 16, child: Divider()),\n          Text(\n            label,\n            style: context.texts.labelMedium?.copyWith(\n              color: context.colors.outline,\n            ),\n          ),\n          const Expanded(child: Divider()),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/widgets/ui/loading_icon.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass LoadingIcon extends StatelessWidget {\n  const LoadingIcon({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const SizedBox(\n      height: 18,\n      width: 18,\n      child: CircularProgressIndicator(strokeWidth: 2),\n    );\n  }\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: dpip\ndescription: \"Disaster Prevention Information Platform\"\npublish_to: \"none\"\nversion: 3.2.1+1\n\nenvironment:\n  sdk: \">=3.10.0 <4.0.0\"\n  flutter: \">=3.38.0\"\n\nplatforms:\n  android:\n  ios:\n\ndependencies:\n  android_alarm_manager_plus:\n    git:\n      url: https://github.com/ExpTechTW/plus_plugins.git\n      path: packages/android_alarm_manager_plus\n      ref: 81fc434\n  async: ^2.13.0\n  awesome_notifications: 0.10.1\n  awesome_notifications_fcm: 0.10.1\n  cached_network_image: ^3.4.1\n  collection: ^1.19.1\n  crypto: ^3.0.7\n  device_info_plus: ^12.3.0\n  disable_battery_optimization:\n    git:\n      url: https://github.com/ExpTechTW/Disable-Battery-Optimizations.git\n      ref: 2addde3\n  dynamic_color: ^1.8.1\n  firebase_core: 3.15.2 #4.0.0 升級最低支援iOS 15\n  firebase_messaging: 15.2.10\n  flex_color_picker: ^3.8.0\n  flutter_compass: ^0.8.1\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  flutter_markdown: ^0.7.7\n  fluttertoast: ^9.0.0\n  freezed_annotation: ^3.1.0\n  gal: ^2.3.2\n  geojson_vi: ^2.2.5\n  geolocator: ^14.0.2\n  geolocator_android: ^5.0.2\n  go_router: ^17.0.1\n  google_fonts: ^8.0.2\n  dio: ^5.0.0\n  dio_cache_interceptor: ^3.5.0\n  html_unescape: ^2.0.0\n  i18n_extension: ^15.1.0\n  in_app_purchase: ^3.2.3\n  in_app_update: ^4.2.5\n  intl: ^0.20.2\n  ip_country_lookup: ^1.0.3\n  json_annotation: ^4.9.0\n  m3e_collection: ^0.3.7\n  maplibre_gl: ^0.25.0\n  markdown_widget: ^2.3.2+8\n  material_symbols_icons: ^4.2892.0\n  option_result: ^3.2.1\n  package_info_plus: ^9.0.1\n  path_provider: ^2.1.5\n  permission_handler: ^12.0.1\n  provider: ^6.1.5\n  shared_preferences: ^2.5.4\n  simple_icons: ^14.6.1\n  skeletonizer: ^2.1.2\n  styled_text: ^9.0.0\n  talker_flutter: ^5.1.9\n  timezone: ^0.11.0\n  url_launcher: ^6.3.2\n  zstandard: ^1.3.29\ndev_dependencies:\n  build_runner: ^2.10.4\n  flutter_test:\n    sdk: flutter\n  go_router_builder: ^4.1.3\n  i18n_extension_importer: ^0.0.6\n  in_app_purchase_platform_interface: ^1.4.0\n  json_serializable: ^6.11.3\n  lint: ^2.8.0\n\nflutter:\n  uses-material-design: true\n  shaders:\n    - shaders/fog.frag\n    - shaders/thunderstorm.frag\n  assets:\n    # localizations\n    - assets/translations/\n    # other\n    - assets/map/\n    - assets/wallpaper/day/\n    - assets/wallpaper/dusk/\n    - assets/wallpaper/night/\n    - assets/DPIP.png\n    - assets/ExpTech.png\n    - assets/box.json\n    - assets/location.json.gz\n    - assets/notify_test.json\n    - assets/time.json.gz\n"
  },
  {
    "path": "shaders/fog.frag",
    "content": "#include <flutter/runtime_effect.glsl>\n\nuniform float iTime;\nuniform vec2 iResolution;\nuniform sampler2D iChannel0;\nuniform float iIntensity;\nuniform float iSpeed;\n\nout vec4 fragColor;\n\nvec2 hash22(vec2 p) {\n    p = vec2(dot(p, vec2(127.1, 311.7)),\n             dot(p, vec2(269.5, 183.3)));\n    return fract(sin(p) * 43758.5453);\n}\n\nfloat noise2D(vec2 p) {\n    vec2 i = floor(p);\n    vec2 f = fract(p);\n    \n    vec2 a = hash22(i);\n    vec2 b = hash22(i + vec2(1.0, 0.0));\n    vec2 c = hash22(i + vec2(0.0, 1.0));\n    vec2 d = hash22(i + vec2(1.0, 1.0));\n    \n    vec2 u = f * f * (3.0 - 2.0 * f);\n    \n    return mix(\n        mix(a.x, b.x, u.x),\n        mix(c.x, d.x, u.x),\n        u.y\n    );\n}\n\nfloat fbm(vec2 p) {\n    float value = 0.0;\n    float amplitude = 0.5;\n    float frequency = 1.0;\n    \n    mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);\n    \n    for (int i = 0; i < 6; i++) {\n        value += amplitude * noise2D(p * frequency);\n        amplitude *= 0.5;\n        frequency *= 2.0;\n        p = rot * p;\n    }\n    \n    return value;\n}\n\nfloat fogMask(vec2 uv) {\n    vec2 p = uv * 3.0;\n    p.x -= iTime * iSpeed * 0.1;\n    p.y += iTime * iSpeed * 0.02;\n    \n    float fog = fbm(p);\n    \n    float fog2 = fbm(p * 1.5 - vec2(iTime * iSpeed * 0.12, 0.0));\n    fog = mix(fog, fog2, 0.5);\n    \n    fog = smoothstep(0.2, 0.8, fog);\n    \n    return fog * iIntensity;\n}\n\nvoid main() {\n    vec2 fragCoord = FlutterFragCoord();\n    vec2 uv = fragCoord.xy / iResolution.xy;\n    \n    vec4 originalColor = texture(iChannel0, uv);\n    \n    float fog = fogMask(uv);\n    \n    vec3 fogColor = vec3(0.95, 0.97, 1.0);\n    \n    vec3 finalColor = mix(originalColor.rgb, fogColor, fog);\n    \n    fragColor = vec4(finalColor, originalColor.a);\n}\n\n"
  },
  {
    "path": "shaders/thunderstorm.frag",
    "content": "#include <flutter/runtime_effect.glsl>\n\nuniform float iTime;\nuniform vec2 iResolution;\nuniform float iLightningIntensity;\nuniform float iRainAmount;\nuniform sampler2D iChannel0;\n\nout vec4 fragColor;\n\nvec2 hash22(vec2 p) {\n    vec2 p2 = fract(p * vec2(0.1031, 0.1030));\n    p2 += dot(p2, p2.yx + 19.19);\n    return fract((p2.x + p2.y) * p2);\n}\n\n#define round(x) floor((x) + 0.5)\n\nfloat simplex2D(vec2 p) {\n    const float K1 = (sqrt(3.0) - 1.0) / 2.0;\n    const float K2 = (3.0 - sqrt(3.0)) / 6.0;\n    const float K3 = K2 * 2.0;\n\n    vec2 i = floor(p + dot(p, vec2(K1)));\n    \n    vec2 a = p - i + dot(i, vec2(K2));\n    vec2 o = 1.0 - clamp((a.yx - a) * 1.e35, 0.0, 1.0);\n    vec2 b = a - o + K2;\n    vec2 c = a - 1.0 + K3;\n\n    vec3 h = clamp(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0, 1.0);\n    \n    h *= h;\n    h *= h;\n\n    vec3 n = vec3(\n        dot(a, hash22(i) - 0.5),\n        dot(b, hash22(i + o) - 0.5),\n        dot(c, hash22(i + 1.0) - 0.5)\n    );\n\n    return dot(n, h) * 140.0;\n}\n\nvec2 wetGlass(vec2 p) {\n    p += simplex2D(p * 0.1) * 3.0;\n    \n    float t = iTime;\n    \n    p *= vec2(0.025, 0.025 * 0.25);\n    \n    p.y -= t * 0.25;\n    \n    vec2 rp = round(p);\n    vec2 dropPos = p - rp;\n    vec2 noise = hash22(rp);\n    \n    dropPos.y *= 4.0;\n    \n    t = t * noise.y + (noise.x * 6.28);\n    \n    vec2 trailPos = vec2(dropPos.x, fract((dropPos.y + t) * 2.0) * 0.5 - 0.25);\n    \n    dropPos.y -= cos(t + cos(t));\n   \n    float trailMask = clamp(-dropPos.y * 2.5 + 0.5, 0.0, 1.0);\n\n    float dropSize = dot(dropPos, dropPos);\n    \n    float trailSize = clamp(trailMask * (-dropPos.y) - 0.5, 0.0, 1.0) + 0.5;\n    trailSize = dot(trailPos, trailPos) * trailSize * trailSize;\n    \n    float drop = clamp(dropSize * -60.0 + 3.0 * noise.y, 0.0, 1.0);\n    float trail = clamp(trailSize * -60.0 + 0.5 * noise.y, 0.0, 1.0);\n    \n    trail *= trailMask;\n    \n    return (drop * dropPos + trailPos * trail) * iRainAmount;\n}\n\nvoid main() {\n    vec2 fragCoord = FlutterFragCoord().xy;\n    vec2 uv = fragCoord.xy / iResolution.xy;\n    \n    uv += wetGlass(fragCoord);\n    \n    vec3 col = texture(iChannel0, uv).rgb;\n    \n    float lt = (iTime + 3.0) * 0.5;\n    float lightning = sin(lt * sin(lt * 10.0));\n    lightning *= pow(max(0.0, sin(lt + sin(lt))), 10.0);\n    col *= 1.0 + lightning * iLightningIntensity * 0.3;\n    \n    fragColor = vec4(col, 1.0);\n}\n"
  },
  {
    "path": "tools/update_translations.sh",
    "content": "#!/bin/bash\n\n# 用於更新 .pot 檔案與生成 zh-Hant.po 的腳本\n# 此腳本應從專案根目錄執行\n# 其他語言的 .po 檔案由 Crowdin 自動同步管理\n\n# 設定語言環境為繁體中文\nexport LC_ALL=zh_TW.UTF-8\n\n# 檢查終端機是否支援顏色\nif [ -t 1 ] && [ -n \"$TERM\" ] && command -v tput >/dev/null 2>&1; then\n    BLUE='\\033[0;34m'\n    YELLOW='\\033[1;33m'\n    RED='\\033[0;31m'\n    RESET='\\033[0m'\nelse\n    BLUE=''\n    YELLOW=''\n    RED=''\n    RESET=''\nfi\n\n# .po 檔案所在目錄\nPO_DIR=\"./assets/translations\"\nPOT_FILE=\"./.crowdin/strings.pot\"\n\n# 執行 i18n 擴充功能匯入器來更新 .pot 檔案\necho -e \"${BLUE}> (1/3) 更新 .pot 檔案...${RESET}\"\ndart run i18n_extension_importer:getstrings --output-file ./.crowdin/strings.pot\necho\n\n# 重新產生 zh-Hant.po（使用固定標頭並直接從 .pot 檔案複製內容）\nZH_HANT_PO=\"$PO_DIR/zh-Hant.po\"\necho -e \"${BLUE}> (2/3) 重新產生 zh-Hant.po...${RESET}\"\n\n# 創建帶有固定標頭的新檔案\ncat > \"$ZH_HANT_PO\" << 'EOF'\nmsgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: dpip\\n\"\n\"X-Crowdin-Project-ID: 696803\\n\"\n\"X-Crowdin-Language: zh-TW\\n\"\n\"X-Crowdin-File: /main/.crowdin/strings.pot\\n\"\n\"X-Crowdin-File-ID: 20\\n\"\n\"Project-Id-Version: dpip\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Language-Team: Chinese Traditional\\n\"\n\"Language: zh_TW\\n\"\n\nEOF\n\n# 從 .pot 檔案複製內容並將 msgstr 設為 msgid 的內容\ngawk '\nBEGIN {\n  collecting_msgid = 0\n  msgid_lines = \"\"\n  msgid_content = \"\"\n}\n\n# 註解行直接印出\n/^#/ { print $0; next }\n\n# 空行直接印出，並重置狀態\n/^$/ {\n  collecting_msgid = 0\n  msgid_lines = \"\"\n  msgid_content = \"\"\n  print \"\"\n  next\n}\n\n# msgid 開始\n/^msgid/ {\n  collecting_msgid = 1\n  msgid_lines = \"\"\n  msgid_content = \"\"\n  print $0\n  \n  # 單行 msgid\n  if ($0 ~ /^msgid \".+\"$/) {\n    msgid_content = substr($0, 7)  # 去掉 \"msgid \" 保留引號\n    collecting_msgid = 0\n  }\n  next\n}\n\n# 多行 msgid 的續行\ncollecting_msgid && /^\"/ {\n  print $0\n  if (msgid_lines == \"\") {\n    msgid_lines = $0\n  } else {\n    msgid_lines = msgid_lines \"\\n\" $0\n  }\n  next\n}\n\n# msgstr 行\n/^msgstr/ {\n  collecting_msgid = 0\n  \n  # 單行情況\n  if (msgid_content != \"\") {\n    print \"msgstr \" msgid_content\n  }\n  # 多行情況\n  else if (msgid_lines != \"\") {\n    print \"msgstr \\\"\\\"\"\n    print msgid_lines\n  }\n  # 空的情況\n  else {\n    print $0\n  }\n  \n  msgid_content = \"\"\n  msgid_lines = \"\"\n  next\n}\n\n# 跳過原本的 msgstr 續行\n/^\"/ && !collecting_msgid { next }\n\n# 其他行直接印出\n{ print $0 }\n' \"$POT_FILE\" >> \"$ZH_HANT_PO\"\n\n# 統一路徑格式\necho -e \"${BLUE}> (3/3) 統一路徑格式...${RESET}\"\nsed -i '' 's|^#: \\([^.]\\)|#: ./\\1|g' \"$ZH_HANT_PO\"\n\necho -e \"${BLUE}> 完成！${RESET}\"\n"
  }
]