diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9fea34..9e75211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,8 @@ name: CI on: push: + branches: + - main paths-ignore: - README.md - README_CN.md @@ -87,6 +89,18 @@ jobs: cmd: yq '.version' 'pubspec.yaml' - name: Print version run: echo ${{ steps.yq.outputs.result }} + - name: Create Tag + id: create_tag + run: | + VERSION="${{ steps.yq.outputs.result }}" + TAG_NAME="v${VERSION%%+*}" + echo "TAG_NAME=$TAG_NAME" >> "$GITHUB_OUTPUT" + echo "Creating new tag $TAG_NAME..." + git tag "$TAG_NAME" + git push origin "$TAG_NAME" + - name: Eextract log + id: extract_log + run: python extract_log.py ${{ steps.create_tag.outputs.TAG_NAME }} - name: Download Windows artifact uses: actions/download-artifact@v4 with: @@ -97,22 +111,13 @@ jobs: with: name: Iris_android path: artifacts - - name: Create Tag - id: create_tag - run: | - VERSION="${{ steps.yq.outputs.result }}" - TAG_NAME="v${VERSION%%+*}" - echo "TAG_NAME=$TAG_NAME" >> "$GITHUB_OUTPUT" - echo "Creating new tag $TAG_NAME..." - git tag "$TAG_NAME" - git push origin "$TAG_NAME" - name: Release uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.create_tag.outputs.TAG_NAME }} - body_path: CHANGELOG.md + body: ${{ steps.extract_log.outputs.result }} draft: false prerelease: false files: | diff --git a/CHANGELOG.md b/CHANGELOG.md index fd13c3f..f2c6eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ +## v1.0.2 +### Changelog +* Support for switching built-in audio tracks +* Reduce package size for Windows version + +### 更新日志 +* 支持切换内置音轨 +* 减小 Windows 版本包体大小 + + +## v1.0.1 ### Changelog * Windows version support auto update ### 更新日志 -* Windows 版本支持自动更新 \ No newline at end of file +* Windows 版本支持自动更新 + + +## v1.0.0 +### Changelog +* Supports WebDAV and local storage video playback + +### 更新日志 +* 支持 WebDAV 和本地存储视频播放 \ No newline at end of file diff --git a/README.md b/README.md index b875c52..8802b8d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ English | [中文](./README_CN.md) ## Download -Go to [Releases](https://github.com/nini22P/Iris/releases) to download. +[Windows](https://github.com/nini22P/Iris/releases/latest/download/Iris_windows.zip) | [Android](https://github.com/nini22P/Iris/releases/latest/download/Iris_android.apk) ## Contribution diff --git a/README_CN.md b/README_CN.md index aa47c87..e6372d1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -15,7 +15,7 @@ ## 下载 -前往 [Releases](https://github.com/nini22P/Iris/releases) 下载。 +[Windows](https://github.com/nini22P/Iris/releases/latest/download/Iris_windows.zip) | [Android](https://github.com/nini22P/Iris/releases/latest/download/Iris_android.apk) ## 贡献 diff --git a/assets/fonts/NotoSansCJKsc-Medium.otf b/assets/fonts/NotoSansCJKsc-Medium.otf deleted file mode 100644 index 5095e7b..0000000 Binary files a/assets/fonts/NotoSansCJKsc-Medium.otf and /dev/null differ diff --git a/extract_log.py b/extract_log.py new file mode 100644 index 0000000..ef8c5bf --- /dev/null +++ b/extract_log.py @@ -0,0 +1,35 @@ +import sys + +def extract_log(version): + try: + with open("CHANGELOG.md", "r", encoding="utf-8") as file: + lines = file.readlines() + except FileNotFoundError: + print("Error: not found CHANGELOG.md") + return + + found = False + changelog_lines = [] + + for line in lines: + if line.startswith(f"## {version}"): + found = True + continue + elif line.startswith("## ") and found: + break + if found: + changelog_lines.append(line) + + while changelog_lines and not changelog_lines[-1].strip(): + changelog_lines.pop() + + output = "".join(changelog_lines).strip() + print(output) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python extract_log.py ") + sys.exit(1) + + version = sys.argv[1] + extract_log(version) diff --git a/lib/hooks/use_player_controller.dart b/lib/hooks/use_player_controller.dart index 93f9f0f..e5f5217 100644 --- a/lib/hooks/use_player_controller.dart +++ b/lib/hooks/use_player_controller.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_zustand/flutter_zustand.dart'; import 'package:iris/hooks/use_player_core.dart'; import 'package:iris/store/use_app_store.dart'; import 'package:iris/store/use_play_queue_store.dart'; @@ -28,8 +29,10 @@ class PlayerController { PlayerController usePlayerController( BuildContext context, PlayerCore playerCore) { - final playQueue = playerCore.playQueue; - final currentIndex = playerCore.currentIndex; + final playQueue = + usePlayQueueStore().select(context, (state) => state.playQueue); + final currentIndex = + usePlayQueueStore().select(context, (state) => state.currentIndex); Future play() async { await useAppStore().updateAutoPlay(true); diff --git a/lib/hooks/use_player_core.dart b/lib/hooks/use_player_core.dart index 82cef09..4047c74 100644 --- a/lib/hooks/use_player_core.dart +++ b/lib/hooks/use_player_core.dart @@ -14,12 +14,12 @@ import 'package:media_kit/media_kit.dart'; class PlayerCore { final Player player; final String title; - final List playQueue; - final int currentIndex; final FileItem? currentFile; final SubtitleTrack subtitle; final List subtitles; final List externalSubtitles; + final AudioTrack audio; + final List audios; final bool playing; final Duration position; final Duration duration; @@ -35,12 +35,12 @@ class PlayerCore { PlayerCore( this.player, this.title, - this.playQueue, - this.currentIndex, this.currentFile, this.subtitle, this.subtitles, this.externalSubtitles, + this.audio, + this.audios, this.playing, this.position, this.duration, @@ -79,18 +79,32 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { Duration buffer = useStream(player.stream.buffer).data ?? Duration.zero; bool completed = useStream(player.stream.completed).data ?? false; double rate = useStream(player.stream.rate).data ?? 1.0; + + Track? track = useStream(player.stream.track).data; + AudioTrack audio = + useMemoized(() => track?.audio ?? AudioTrack.no(), [track?.audio]); + SubtitleTrack subtitle = useMemoized( + () => track?.subtitle ?? SubtitleTrack.no(), [track?.subtitle]); + + Tracks? tracks = useStream(player.stream.tracks).data; + List audios = + useMemoized(() => (tracks?.audio ?? []), [tracks?.audio]); + List subtitles = useMemoized( + () => [...(tracks?.subtitle ?? [])] + ..removeWhere((subtitle) => subtitle == SubtitleTrack.auto()), + [tracks?.subtitle]); + + final List? externalSubtitles = useMemoized( + () => [...currentFile?.subtitles ?? []]..removeWhere( + (subtitle) => subtitles.any((item) => item.title == subtitle.name)), + [currentFile?.subtitles, subtitles]); + VideoParams? videoParams = useStream(player.stream.videoParams).data; double aspectRatio = videoParams != null && videoParams.w != null && videoParams.h != null ? (videoParams.w! / videoParams.h!) : 0; - final subtitle = useState(SubtitleTrack.no()); - final subtitles = useState>([]); - - final List? externalSubtitles = - useMemoized(() => currentFile?.subtitles ?? [], [currentFile]); - final positionStream = useStream(player.stream.position); if (positionStream.hasData) { @@ -142,21 +156,6 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { final cover = useFuture(getCover).data; - useEffect(() { - final subscription = player.stream.track.listen((event) { - subtitle.value = event.subtitle; - }); - return subscription.cancel; - }, []); - - useEffect(() { - final subscription = player.stream.tracks.listen((event) { - subtitles.value = [...event.subtitle]..removeWhere((subtitle) => - [SubtitleTrack.auto(), SubtitleTrack.no()].contains(subtitle)); - }); - return subscription.cancel; - }, []); - useEffect(() { if (currentFile == null || playQueue.isEmpty) return; log('Now playing: ${currentFile.name}, auto play: $autoPlay'); @@ -171,20 +170,27 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { }, [currentFile]); useEffect(() { - if (duration == Duration.zero) return; - if (externalSubtitles!.isNotEmpty) { - log('Set external subtitle: ${externalSubtitles[0].name}'); - player.setSubtitleTrack( - SubtitleTrack.uri( - externalSubtitles[0].uri, - title: externalSubtitles[0].name, - ), - ); - } else if (subtitles.value.isNotEmpty) { - log('Set subtitle: ${subtitles.value[0].title}'); - player.setSubtitleTrack(subtitles.value[0]); - } - return null; + () async { + if (duration == Duration.zero) { + await player.setSubtitleTrack(SubtitleTrack.no()); + return; + } + if (externalSubtitles!.isNotEmpty) { + log('Set external subtitle: ${externalSubtitles[0].name}'); + await player.setSubtitleTrack( + SubtitleTrack.uri( + externalSubtitles[0].uri, + title: externalSubtitles[0].name, + ), + ); + } else if (subtitles.length > 1) { + log('Set subtitle: ${subtitles[1].title ?? subtitles[1].language ?? subtitles[1].id}'); + await player.setSubtitleTrack(subtitles[1]); + } else { + await player.setSubtitleTrack(SubtitleTrack.no()); + } + }(); + return; }, [duration]); void updatePosition(Duration newPosition) => position.value = newPosition; @@ -194,12 +200,12 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { return PlayerCore( player, title, - playQueue, - currentIndex, currentFile, - subtitle.value, - subtitles.value, + subtitle, + subtitles, externalSubtitles ?? [], + audio, + audios, playing, duration == Duration.zero ? Duration.zero : position.value, duration, diff --git a/lib/info.dart b/lib/info.dart index 6e9fddd..6c7a066 100644 --- a/lib/info.dart +++ b/lib/info.dart @@ -1,6 +1,5 @@ class INFO { static const String title = 'Iris'; - static const String description = 'A lightweight video player'; static const String author = '22'; static const String authorUrl = 'https://github.com/nini22P'; static const String githubUrl = 'https://github.com/nini22P/Iris'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9f3bdc6..216c6e3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,63 +1,66 @@ { "@@locale": "en", - "ok": "OK", - "cancel": "Cancel", - "close": "Close", - "add": "Add", - "edit": "Edit", - "save": "Save", - "remove": "Remove", - "name": "Name", - "url": "URL", - "path": "Path", - "port": "Port", - "username": "Username", - "password": "Password", - "storages": "Storages", - "add_storage": "Add Storage", - "favorites": "Favorites", - "add_favorite": "Add Favorite", - "remove_favorite": "Remove Favorite", - "local_storage": "Local Storage", - "connection_test": "Connection Test", - "subtitles": "Subtitles", - "open_file": "Open File", - "open_link": "Open Link", - "play": "Play", - "pause": "Pause", - "previous": "Previous", - "next": "Next", - "play_queue": "Play Queue", - "enter_fullscreen": "Enter Fullscreen", - "exit_fullscreen": "Exit Fullscreen", - "version": "Version", - "source_code": "Source Code", - "author": "Author", - "theme_mode": "Theme Mode", - "auto": "Auto", - "light": "Light", - "dark": "Datk", - "settings": "Settings", - "general": "General", "about": "About", - "libraries": "Libraries", - "back": "Back", - "home": "Home", - "menu": "Menu", - "off": "Off", - "unable_to_fetch_files": "Unable to fetch files.", - "language": "Language", - "select_language": "Select Language", + "add": "Add", + "add_favorite": "Add Favorite", "add_local_storage": "Add Local Storage", - "edit_local_storage": "Edit Local Storage", + "add_storage": "Add Storage", "add_webdav_storage": "Add WebDAV Storage", - "edit_webdav_storage": "Edit WebDAV Storage", - "auto_resize": "Auto Resize Window Proportion", - "always_on_top_on": "Always On Top: On", "always_on_top_off": "Always On Top: Off", + "always_on_top_on": "Always On Top: On", + "app_description": "A lightweight video player", + "audio_track": "Audio Track", + "author": "Author", + "auto": "Auto", + "auto_resize": "Auto Resize Window Proportion", + "back": "Back", + "cancel": "Cancel", "check_update": "Check Update", + "checked_new_version": "Checked New Version", + "close": "Close", + "connection_test": "Connection Test", + "dark": "Datk", "download": "Download", "download_and_update": "Download and Update", - "checked_new_version": "Checked New Version", - "no_new_version": "No New Version" + "edit": "Edit", + "edit_local_storage": "Edit Local Storage", + "edit_webdav_storage": "Edit WebDAV Storage", + "enter_fullscreen": "Enter Fullscreen", + "exit_fullscreen": "Exit Fullscreen", + "favorites": "Favorites", + "general": "General", + "home": "Home", + "language": "Language", + "libraries": "Libraries", + "light": "Light", + "local_storage": "Local Storage", + "menu": "Menu", + "name": "Name", + "next": "Next", + "no_new_version": "No New Version", + "off": "Off", + "ok": "OK", + "open_file": "Open File", + "open_link": "Open Link", + "password": "Password", + "path": "Path", + "pause": "Pause", + "play": "Play", + "play_queue": "Play Queue", + "port": "Port", + "previous": "Previous", + "remove": "Remove", + "remove_favorite": "Remove Favorite", + "save": "Save", + "select_language": "Select Language", + "settings": "Settings", + "source_code": "Source Code", + "storages": "Storages", + "subtitle": "Subtitle", + "subtitle_and_audio_track": "Subtitle and Audio Track", + "theme_mode": "Theme Mode", + "unable_to_fetch_files": "Unable to fetch files.", + "url": "URL", + "username": "Username", + "version": "Version" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0f9796d..1f305c1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,63 +1,66 @@ { "@@locale": "zh", - "ok": "确定", - "cancel": "取消", - "close": "关闭", - "add": "添加", - "edit": "编辑", - "save": "保存", - "remove": "移除", - "name": "名称", - "url": "URL", - "path": "路径", - "port": "端口", - "username": "用户名", - "password": "密码", - "storages": "存储", - "add_storage": "添加存储", - "favorites": "收藏", - "add_favorite": "添加收藏", - "remove_favorite": "移除收藏", - "local_storage": "本地存储", - "connection_test": "连接测试", - "subtitles": "字幕", - "open_file": "打开文件", - "open_link": "打开链接", - "play": "播放", - "pause": "暂停", - "previous": "上一个", - "next": "下一个", - "play_queue": "播放队列", - "enter_fullscreen": "进入全屏", - "exit_fullscreen": "退出全屏", - "version": "版本", - "source_code": "源码", - "author": "作者", - "theme_mode": "主题模式", - "auto": "自动", - "light": "亮色", - "dark": "暗色", - "settings": "设置", - "general": "通用", "about": "关于", - "libraries": "开源库", - "back": "返回", - "home": "主页", - "menu": "菜单", - "off": "关闭", - "unable_to_fetch_files": "无法获取文件", - "language": "语言", - "select_language": "选择语言", + "add": "添加", + "add_favorite": "添加收藏", "add_local_storage": "添加本地存储", - "edit_local_storage": "编辑本地存储", + "add_storage": "添加存储", "add_webdav_storage": "添加 WebDAV 存储", - "edit_webdav_storage": "编辑 WebDAV 存储", - "auto_resize": "自动调整窗口比例", - "always_on_top_on": "窗口置顶: 开", "always_on_top_off": "窗口置顶: 关", + "always_on_top_on": "窗口置顶: 开", + "app_description": "轻量级视频播放器", + "audio_track": "音轨", + "author": "作者", + "auto": "自动", + "auto_resize": "自动调整窗口比例", + "back": "返回", + "cancel": "取消", "check_update": "检查更新", + "checked_new_version": "检查到新版本", + "close": "关闭", + "connection_test": "连接测试", + "dark": "暗色", "download": "下载", "download_and_update": "下载并更新", - "checked_new_version": "检查到新版本", - "no_new_version": "没有新版本" + "edit": "编辑", + "edit_local_storage": "编辑本地存储", + "edit_webdav_storage": "编辑 WebDAV 存储", + "enter_fullscreen": "进入全屏", + "exit_fullscreen": "退出全屏", + "favorites": "收藏", + "general": "通用", + "home": "主页", + "language": "语言", + "libraries": "开源库", + "light": "亮色", + "local_storage": "本地存储", + "menu": "菜单", + "name": "名称", + "next": "下一个", + "no_new_version": "没有新版本", + "off": "关闭", + "ok": "确定", + "open_file": "打开文件", + "open_link": "打开链接", + "password": "密码", + "path": "路径", + "pause": "暂停", + "play": "播放", + "play_queue": "播放队列", + "port": "端口", + "previous": "上一个", + "remove": "移除", + "remove_favorite": "移除收藏", + "save": "保存", + "select_language": "选择语言", + "settings": "设置", + "source_code": "源码", + "storages": "存储", + "subtitle": "字幕", + "subtitle_and_audio_track": "字幕和音轨", + "theme_mode": "主题模式", + "unable_to_fetch_files": "无法获取文件", + "url": "URL", + "username": "用户名", + "version": "版本" } \ No newline at end of file diff --git a/lib/l10n/iso_639.dart b/lib/l10n/iso_639.dart new file mode 100644 index 0000000..f00489e --- /dev/null +++ b/lib/l10n/iso_639.dart @@ -0,0 +1,720 @@ +class Info { + final List en; + + Info({required this.en}); +} + +Map customLanguageCodes = { + 'chs': Info(en: ['Chinese (Simplified)']), + 'cht': Info(en: ['Chinese (Traditional)']), +}; + +Map iso_639_1 = { + 'aa': Info(en: ['Afar']), + 'ab': Info(en: ['Abkhazian']), + 'ae': Info(en: ['Avestan']), + 'af': Info(en: ['Afrikaans']), + 'ak': Info(en: ['Akan']), + 'am': Info(en: ['Amharic']), + 'an': Info(en: ['Aragonese']), + 'ar': Info(en: ['Arabic']), + 'as': Info(en: ['Assamese']), + 'av': Info(en: ['Avaric']), + 'ay': Info(en: ['Aymara']), + 'az': Info(en: ['Azerbaijani']), + 'ba': Info(en: ['Bashkir']), + 'be': Info(en: ['Belarusian']), + 'bg': Info(en: ['Bulgarian']), + 'bh': Info(en: ['Bihari languages']), + 'bi': Info(en: ['Bislama']), + 'bm': Info(en: ['Bambara']), + 'bn': Info(en: ['Bengali']), + 'bo': Info(en: ['Tibetan']), + 'br': Info(en: ['Breton']), + 'bs': Info(en: ['Bosnian']), + 'ca': Info(en: ['Catalan', 'Valencian']), + 'ce': Info(en: ['Chechen']), + 'ch': Info(en: ['Chamorro']), + 'co': Info(en: ['Corsican']), + 'cr': Info(en: ['Cree']), + 'cs': Info(en: ['Czech']), + 'cu': Info(en: [ + 'Church Slavic', + 'Old Slavonic', + 'Church Slavonic', + 'Old Bulgarian', + 'Old Church Slavonic' + ]), + 'cv': Info(en: ['Chuvash']), + 'cy': Info(en: ['Welsh']), + 'da': Info(en: ['Danish']), + 'de': Info(en: ['German']), + 'dv': Info(en: ['Divehi', 'Dhivehi', 'Maldivian']), + 'dz': Info(en: ['Dzongkha']), + 'ee': Info(en: ['Ewe']), + 'el': Info(en: ['Greek, Modern (1453-)']), + 'en': Info(en: ['English']), + 'eo': Info(en: ['Esperanto']), + 'es': Info(en: ['Spanish', 'Castilian']), + 'et': Info(en: ['Estonian']), + 'eu': Info(en: ['Basque']), + 'fa': Info(en: ['Persian']), + 'ff': Info(en: ['Fulah']), + 'fi': Info(en: ['Finnish']), + 'fj': Info(en: ['Fijian']), + 'fo': Info(en: ['Faroese']), + 'fr': Info(en: ['French']), + 'fy': Info(en: ['Western Frisian']), + 'ga': Info(en: ['Irish']), + 'gd': Info(en: ['Gaelic', 'Scottish Gaelic']), + 'gl': Info(en: ['Galician']), + 'gn': Info(en: ['Guarani']), + 'gu': Info(en: ['Gujarati']), + 'gv': Info(en: ['Manx']), + 'ha': Info(en: ['Hausa']), + 'he': Info(en: ['Hebrew']), + 'hi': Info(en: ['Hindi']), + 'ho': Info(en: ['Hiri Motu']), + 'hr': Info(en: ['Croatian']), + 'ht': Info(en: ['Haitian', 'Haitian Creole']), + 'hu': Info(en: ['Hungarian']), + 'hy': Info(en: ['Armenian']), + 'hz': Info(en: ['Herero']), + 'ia': + Info(en: ['Interlingua (International Auxiliary Language Association)']), + 'id': Info(en: ['Indonesian']), + 'ie': Info(en: ['Interlingue', 'Occidental']), + 'ig': Info(en: ['Igbo']), + 'ii': Info(en: ['Sichuan Yi', 'Nuosu']), + 'ik': Info(en: ['Inupiaq']), + 'io': Info(en: ['Ido']), + 'is': Info(en: ['Icelandic']), + 'it': Info(en: ['Italian']), + 'iu': Info(en: ['Inuktitut']), + 'ja': Info(en: ['Japanese']), + 'jv': Info(en: ['Javanese']), + 'ka': Info(en: ['Georgian']), + 'kg': Info(en: ['Kongo']), + 'ki': Info(en: ['Kikuyu', 'Gikuyu']), + 'kj': Info(en: ['Kuanyama', 'Kwanyama']), + 'kk': Info(en: ['Kazakh']), + 'kl': Info(en: ['Kalaallisut', 'Greenlandic']), + 'km': Info(en: ['Central Khmer']), + 'kn': Info(en: ['Kannada']), + 'ko': Info(en: ['Korean']), + 'kr': Info(en: ['Kanuri']), + 'ks': Info(en: ['Kashmiri']), + 'ku': Info(en: ['Kurdish']), + 'kv': Info(en: ['Komi']), + 'kw': Info(en: ['Cornish']), + 'ky': Info(en: ['Kirghiz', 'Kyrgyz']), + 'la': Info(en: ['Latin']), + 'lb': Info(en: ['Luxembourgish', 'Letzeburgesch']), + 'lg': Info(en: ['Ganda']), + 'li': Info(en: ['Limburgan', 'Limburger', 'Limburgish']), + 'ln': Info(en: ['Lingala']), + 'lo': Info(en: ['Lao']), + 'lt': Info(en: ['Lithuanian']), + 'lu': Info(en: ['Luba-Katanga']), + 'lv': Info(en: ['Latvian']), + 'mg': Info(en: ['Malagasy']), + 'mh': Info(en: ['Marshallese']), + 'mi': Info(en: ['Maori']), + 'mk': Info(en: ['Macedonian']), + 'ml': Info(en: ['Malayalam']), + 'mn': Info(en: ['Mongolian']), + 'mr': Info(en: ['Marathi']), + 'ms': Info(en: ['Malay']), + 'mt': Info(en: ['Maltese']), + 'my': Info(en: ['Burmese']), + 'na': Info(en: ['Nauru']), + 'nb': Info(en: ['Bokmål, Norwegian', 'Norwegian Bokmål']), + 'nd': Info(en: ['Ndebele, North', 'North Ndebele']), + 'ne': Info(en: ['Nepali']), + 'ng': Info(en: ['Ndonga']), + 'nl': Info(en: ['Dutch', 'Flemish']), + 'nn': Info(en: ['Norwegian Nynorsk', 'Nynorsk, Norwegian']), + 'no': Info(en: ['Norwegian']), + 'nr': Info(en: ['Ndebele, South', 'South Ndebele']), + 'nv': Info(en: ['Navajo', 'Navaho']), + 'ny': Info(en: ['Chichewa', 'Chewa', 'Nyanja']), + 'oc': Info(en: ['Occitan (post 1500)']), + 'oj': Info(en: ['Ojibwa']), + 'om': Info(en: ['Oromo']), + 'or': Info(en: ['Oriya']), + 'os': Info(en: ['Ossetian', 'Ossetic']), + 'pa': Info(en: ['Panjabi', 'Punjabi']), + 'pi': Info(en: ['Pali']), + 'pl': Info(en: ['Polish']), + 'ps': Info(en: ['Pushto', 'Pashto']), + 'pt': Info(en: ['Portuguese']), + 'qu': Info(en: ['Quechua']), + 'rm': Info(en: ['Romansh']), + 'rn': Info(en: ['Rundi']), + 'ro': Info(en: ['Romanian', 'Moldavian', 'Moldovan']), + 'ru': Info(en: ['Russian']), + 'rw': Info(en: ['Kinyarwanda']), + 'sa': Info(en: ['Sanskrit']), + 'sc': Info(en: ['Sardinian']), + 'sd': Info(en: ['Sindhi']), + 'se': Info(en: ['Northern Sami']), + 'sg': Info(en: ['Sango']), + 'si': Info(en: ['Sinhala', 'Sinhalese']), + 'sk': Info(en: ['Slovak']), + 'sl': Info(en: ['Slovenian']), + 'sm': Info(en: ['Samoan']), + 'sn': Info(en: ['Shona']), + 'so': Info(en: ['Somali']), + 'sq': Info(en: ['Albanian']), + 'sr': Info(en: ['Serbian']), + 'ss': Info(en: ['Swati']), + 'st': Info(en: ['Sotho, Southern']), + 'su': Info(en: ['Sundanese']), + 'sv': Info(en: ['Swedish']), + 'sw': Info(en: ['Swahili']), + 'ta': Info(en: ['Tamil']), + 'te': Info(en: ['Telugu']), + 'tg': Info(en: ['Tajik']), + 'th': Info(en: ['Thai']), + 'ti': Info(en: ['Tigrinya']), + 'tk': Info(en: ['Turkmen']), + 'tl': Info(en: ['Tagalog']), + 'tn': Info(en: ['Tswana']), + 'to': Info(en: ['Tonga (Tonga Islands)']), + 'tr': Info(en: ['Turkish']), + 'ts': Info(en: ['Tsonga']), + 'tt': Info(en: ['Tatar']), + 'tw': Info(en: ['Twi']), + 'ty': Info(en: ['Tahitian']), + 'ug': Info(en: ['Uighur', 'Uyghur']), + 'uk': Info(en: ['Ukrainian']), + 'ur': Info(en: ['Urdu']), + 'uz': Info(en: ['Uzbek']), + 've': Info(en: ['Venda']), + 'vi': Info(en: ['Vietnamese']), + 'vo': Info(en: ['Volapük']), + 'wa': Info(en: ['Walloon']), + 'wo': Info(en: ['Wolof']), + 'xh': Info(en: ['Xhosa']), + 'yi': Info(en: ['Yiddish']), + 'yo': Info(en: ['Yoruba']), + 'za': Info(en: ['Zhuang', 'Chuang']), + 'zh': Info(en: ['Chinese']), + 'zu': Info(en: ['Zulu']), +}; + +Map iso_639_2 = { + 'aar': Info(en: ['Afar']), + 'abk': Info(en: ['Abkhazian']), + 'ace': Info(en: ['Achinese']), + 'ach': Info(en: ['Acoli']), + 'ada': Info(en: ['Adangme']), + 'ady': Info(en: ['Adyghe', 'Adygei']), + 'afa': Info(en: ['Afro-Asiatic languages']), + 'afh': Info(en: ['Afrihili']), + 'afr': Info(en: ['Afrikaans']), + 'ain': Info(en: ['Ainu']), + 'aka': Info(en: ['Akan']), + 'akk': Info(en: ['Akkadian']), + 'alb': Info(en: ['Albanian']), + 'ale': Info(en: ['Aleut']), + 'alg': Info(en: ['Algonquian languages']), + 'alt': Info(en: ['Southern Altai']), + 'amh': Info(en: ['Amharic']), + 'ang': Info(en: ['English, Old (ca.450-1100)']), + 'anp': Info(en: ['Angika']), + 'apa': Info(en: ['Apache languages']), + 'ara': Info(en: ['Arabic']), + 'arc': Info( + en: ['Official Aramaic (700-300 BCE)', 'Imperial Aramaic (700-300 BCE)']), + 'arg': Info(en: ['Aragonese']), + 'arm': Info(en: ['Armenian']), + 'arn': Info(en: ['Mapudungun', 'Mapuche']), + 'arp': Info(en: ['Arapaho']), + 'art': Info(en: ['Artificial languages']), + 'arw': Info(en: ['Arawak']), + 'asm': Info(en: ['Assamese']), + 'ast': Info(en: ['Asturian', 'Bable', 'Leonese', 'Asturleonese']), + 'ath': Info(en: ['Athapascan languages']), + 'aus': Info(en: ['Australian languages']), + 'ava': Info(en: ['Avaric']), + 'ave': Info(en: ['Avestan']), + 'awa': Info(en: ['Awadhi']), + 'aym': Info(en: ['Aymara']), + 'aze': Info(en: ['Azerbaijani']), + 'bad': Info(en: ['Banda languages']), + 'bai': Info(en: ['Bamileke languages']), + 'bak': Info(en: ['Bashkir']), + 'bal': Info(en: ['Baluchi']), + 'bam': Info(en: ['Bambara']), + 'ban': Info(en: ['Balinese']), + 'baq': Info(en: ['Basque']), + 'bas': Info(en: ['Basa']), + 'bat': Info(en: ['Baltic languages']), + 'bej': Info(en: ['Beja', 'Bedawiyet']), + 'bel': Info(en: ['Belarusian']), + 'bem': Info(en: ['Bemba']), + 'ben': Info(en: ['Bengali']), + 'ber': Info(en: ['Berber languages']), + 'bho': Info(en: ['Bhojpuri']), + 'bih': Info(en: ['Bihari languages']), + 'bik': Info(en: ['Bikol']), + 'bin': Info(en: ['Bini', 'Edo']), + 'bis': Info(en: ['Bislama']), + 'bla': Info(en: ['Siksika']), + 'bnt': Info(en: ['Bantu languages']), + 'bod': Info(en: ['Tibetan']), + 'bos': Info(en: ['Bosnian']), + 'bra': Info(en: ['Braj']), + 'bre': Info(en: ['Breton']), + 'btk': Info(en: ['Batak languages']), + 'bua': Info(en: ['Buriat']), + 'bug': Info(en: ['Buginese']), + 'bul': Info(en: ['Bulgarian']), + 'bur': Info(en: ['Burmese']), + 'byn': Info(en: ['Blin', 'Bilin']), + 'cad': Info(en: ['Caddo']), + 'cai': Info(en: ['Central American Indian languages']), + 'car': Info(en: ['Galibi Carib']), + 'cat': Info(en: ['Catalan', 'Valencian']), + 'cau': Info(en: ['Caucasian languages']), + 'ceb': Info(en: ['Cebuano']), + 'cel': Info(en: ['Celtic languages']), + 'ces': Info(en: ['Czech']), + 'cha': Info(en: ['Chamorro']), + 'chb': Info(en: ['Chibcha']), + 'che': Info(en: ['Chechen']), + 'chg': Info(en: ['Chagatai']), + 'chi': Info(en: ['Chinese']), + 'chk': Info(en: ['Chuukese']), + 'chm': Info(en: ['Mari']), + 'chn': Info(en: ['Chinook jargon']), + 'cho': Info(en: ['Choctaw']), + 'chp': Info(en: ['Chipewyan', 'Dene Suline']), + 'chr': Info(en: ['Cherokee']), + 'chu': Info(en: [ + 'Church Slavic', + 'Old Slavonic', + 'Church Slavonic', + 'Old Bulgarian', + 'Old Church Slavonic' + ]), + 'chv': Info(en: ['Chuvash']), + 'chy': Info(en: ['Cheyenne']), + 'cmc': Info(en: ['Chamic languages']), + 'cop': Info(en: ['Coptic']), + 'cor': Info(en: ['Cornish']), + 'cos': Info(en: ['Corsican']), + 'cpe': Info(en: ['Creoles and pidgins, English based']), + 'cpf': Info(en: ['Creoles and pidgins, French-based']), + 'cpp': Info(en: ['Creoles and pidgins, Portuguese-based']), + 'cre': Info(en: ['Cree']), + 'crh': Info(en: ['Crimean Tatar', 'Crimean Turkish']), + 'crp': Info(en: ['Creoles and pidgins']), + 'csb': Info(en: ['Kashubian']), + 'cus': Info(en: ['Cushitic languages']), + 'cym': Info(en: ['Welsh']), + 'cze': Info(en: ['Czech']), + 'dak': Info(en: ['Dakota']), + 'dan': Info(en: ['Danish']), + 'dar': Info(en: ['Dargwa']), + 'day': Info(en: ['Land Dayak languages']), + 'del': Info(en: ['Delaware']), + 'den': Info(en: ['Slave (Athapascan)']), + 'deu': Info(en: ['German']), + 'dgr': Info(en: ['Dogrib']), + 'din': Info(en: ['Dinka']), + 'div': Info(en: ['Divehi', 'Dhivehi', 'Maldivian']), + 'doi': Info(en: ['Dogri']), + 'dra': Info(en: ['Dravidian languages']), + 'dsb': Info(en: ['Lower Sorbian']), + 'dua': Info(en: ['Duala']), + 'dum': Info(en: ['Dutch, Middle (ca.1050-1350)']), + 'dut': Info(en: ['Dutch', 'Flemish']), + 'dyu': Info(en: ['Dyula']), + 'dzo': Info(en: ['Dzongkha']), + 'efi': Info(en: ['Efik']), + 'egy': Info(en: ['Egyptian (Ancient)']), + 'eka': Info(en: ['Ekajuk']), + 'ell': Info(en: ['Greek, Modern (1453-)']), + 'elx': Info(en: ['Elamite']), + 'eng': Info(en: ['English']), + 'enm': Info(en: ['English, Middle (1100-1500)']), + 'epo': Info(en: ['Esperanto']), + 'est': Info(en: ['Estonian']), + 'eus': Info(en: ['Basque']), + 'ewe': Info(en: ['Ewe']), + 'ewo': Info(en: ['Ewondo']), + 'fan': Info(en: ['Fang']), + 'fao': Info(en: ['Faroese']), + 'fas': Info(en: ['Persian']), + 'fat': Info(en: ['Fanti']), + 'fij': Info(en: ['Fijian']), + 'fil': Info(en: ['Filipino', 'Pilipino']), + 'fin': Info(en: ['Finnish']), + 'fiu': Info(en: ['Finno-Ugrian languages']), + 'fon': Info(en: ['Fon']), + 'fra': Info(en: ['French']), + 'fre': Info(en: ['French']), + 'frm': Info(en: ['French, Middle (ca.1400-1600)']), + 'fro': Info(en: ['French, Old (842-ca.1400)']), + 'frr': Info(en: ['Northern Frisian']), + 'frs': Info(en: ['Eastern Frisian']), + 'fry': Info(en: ['Western Frisian']), + 'ful': Info(en: ['Fulah']), + 'fur': Info(en: ['Friulian']), + 'gaa': Info(en: ['Ga']), + 'gay': Info(en: ['Gayo']), + 'gba': Info(en: ['Gbaya']), + 'gem': Info(en: ['Germanic languages']), + 'geo': Info(en: ['Georgian']), + 'ger': Info(en: ['German']), + 'gez': Info(en: ['Geez']), + 'gil': Info(en: ['Gilbertese']), + 'gla': Info(en: ['Gaelic', 'Scottish Gaelic']), + 'gle': Info(en: ['Irish']), + 'glg': Info(en: ['Galician']), + 'glv': Info(en: ['Manx']), + 'gmh': Info(en: ['German, Middle High (ca.1050-1500)']), + 'goh': Info(en: ['German, Old High (ca.750-1050)']), + 'gon': Info(en: ['Gondi']), + 'gor': Info(en: ['Gorontalo']), + 'got': Info(en: ['Gothic']), + 'grb': Info(en: ['Grebo']), + 'grc': Info(en: ['Greek, Ancient (to 1453)']), + 'gre': Info(en: ['Greek, Modern (1453-)']), + 'grn': Info(en: ['Guarani']), + 'gsw': Info(en: ['Swiss German', 'Alemannic', 'Alsatian']), + 'guj': Info(en: ['Gujarati']), + 'gwi': Info(en: ['Gwich\'in']), + 'hai': Info(en: ['Haida']), + 'hat': Info(en: ['Haitian', 'Haitian Creole']), + 'hau': Info(en: ['Hausa']), + 'haw': Info(en: ['Hawaiian']), + 'heb': Info(en: ['Hebrew']), + 'her': Info(en: ['Herero']), + 'hil': Info(en: ['Hiligaynon']), + 'him': Info(en: ['Himachali languages', 'Western Pahari languages']), + 'hin': Info(en: ['Hindi']), + 'hit': Info(en: ['Hittite']), + 'hmn': Info(en: ['Hmong', 'Mong']), + 'hmo': Info(en: ['Hiri Motu']), + 'hrv': Info(en: ['Croatian']), + 'hsb': Info(en: ['Upper Sorbian']), + 'hun': Info(en: ['Hungarian']), + 'hup': Info(en: ['Hupa']), + 'hye': Info(en: ['Armenian']), + 'iba': Info(en: ['Iban']), + 'ibo': Info(en: ['Igbo']), + 'ice': Info(en: ['Icelandic']), + 'ido': Info(en: ['Ido']), + 'iii': Info(en: ['Sichuan Yi', 'Nuosu']), + 'ijo': Info(en: ['Ijo languages']), + 'iku': Info(en: ['Inuktitut']), + 'ile': Info(en: ['Interlingue', 'Occidental']), + 'ilo': Info(en: ['Iloko']), + 'ina': + Info(en: ['Interlingua (International Auxiliary Language Association)']), + 'inc': Info(en: ['Indic languages']), + 'ind': Info(en: ['Indonesian']), + 'ine': Info(en: ['Indo-European languages']), + 'inh': Info(en: ['Ingush']), + 'ipk': Info(en: ['Inupiaq']), + 'ira': Info(en: ['Iranian languages']), + 'iro': Info(en: ['Iroquoian languages']), + 'isl': Info(en: ['Icelandic']), + 'ita': Info(en: ['Italian']), + 'jav': Info(en: ['Javanese']), + 'jbo': Info(en: ['Lojban']), + 'jpn': Info(en: ['Japanese']), + 'jpr': Info(en: ['Judeo-Persian']), + 'jrb': Info(en: ['Judeo-Arabic']), + 'kaa': Info(en: ['Kara-Kalpak']), + 'kab': Info(en: ['Kabyle']), + 'kac': Info(en: ['Kachin', 'Jingpho']), + 'kal': Info(en: ['Kalaallisut', 'Greenlandic']), + 'kam': Info(en: ['Kamba']), + 'kan': Info(en: ['Kannada']), + 'kar': Info(en: ['Karen languages']), + 'kas': Info(en: ['Kashmiri']), + 'kat': Info(en: ['Georgian']), + 'kau': Info(en: ['Kanuri']), + 'kaw': Info(en: ['Kawi']), + 'kaz': Info(en: ['Kazakh']), + 'kbd': Info(en: ['Kabardian']), + 'kha': Info(en: ['Khasi']), + 'khi': Info(en: ['Khoisan languages']), + 'khm': Info(en: ['Central Khmer']), + 'kho': Info(en: ['Khotanese', 'Sakan']), + 'kik': Info(en: ['Kikuyu', 'Gikuyu']), + 'kin': Info(en: ['Kinyarwanda']), + 'kir': Info(en: ['Kirghiz', 'Kyrgyz']), + 'kmb': Info(en: ['Kimbundu']), + 'kok': Info(en: ['Konkani']), + 'kom': Info(en: ['Komi']), + 'kon': Info(en: ['Kongo']), + 'kor': Info(en: ['Korean']), + 'kos': Info(en: ['Kosraean']), + 'kpe': Info(en: ['Kpelle']), + 'krc': Info(en: ['Karachay-Balkar']), + 'krl': Info(en: ['Karelian']), + 'kro': Info(en: ['Kru languages']), + 'kru': Info(en: ['Kurukh']), + 'kua': Info(en: ['Kuanyama', 'Kwanyama']), + 'kum': Info(en: ['Kumyk']), + 'kur': Info(en: ['Kurdish']), + 'kut': Info(en: ['Kutenai']), + 'lad': Info(en: ['Ladino']), + 'lah': Info(en: ['Lahnda']), + 'lam': Info(en: ['Lamba']), + 'lao': Info(en: ['Lao']), + 'lat': Info(en: ['Latin']), + 'lav': Info(en: ['Latvian']), + 'lez': Info(en: ['Lezghian']), + 'lim': Info(en: ['Limburgan', 'Limburger', 'Limburgish']), + 'lin': Info(en: ['Lingala']), + 'lit': Info(en: ['Lithuanian']), + 'lol': Info(en: ['Mongo']), + 'loz': Info(en: ['Lozi']), + 'ltz': Info(en: ['Luxembourgish', 'Letzeburgesch']), + 'lua': Info(en: ['Luba-Lulua']), + 'lub': Info(en: ['Luba-Katanga']), + 'lug': Info(en: ['Ganda']), + 'lui': Info(en: ['Luiseno']), + 'lun': Info(en: ['Lunda']), + 'luo': Info(en: ['Luo (Kenya and Tanzania)']), + 'lus': Info(en: ['Lushai']), + 'mac': Info(en: ['Macedonian']), + 'mad': Info(en: ['Madurese']), + 'mag': Info(en: ['Magahi']), + 'mah': Info(en: ['Marshallese']), + 'mai': Info(en: ['Maithili']), + 'mak': Info(en: ['Makasar']), + 'mal': Info(en: ['Malayalam']), + 'man': Info(en: ['Mandingo']), + 'mao': Info(en: ['Maori']), + 'map': Info(en: ['Austronesian languages']), + 'mar': Info(en: ['Marathi']), + 'mas': Info(en: ['Masai']), + 'may': Info(en: ['Malay']), + 'mdf': Info(en: ['Moksha']), + 'mdr': Info(en: ['Mandar']), + 'men': Info(en: ['Mende']), + 'mga': Info(en: ['Irish, Middle (900-1200)']), + 'mic': Info(en: ['Mi\'kmaq', 'Micmac']), + 'min': Info(en: ['Minangkabau']), + 'mis': Info(en: ['Uncoded languages']), + 'mkd': Info(en: ['Macedonian']), + 'mkh': Info(en: ['Mon-Khmer languages']), + 'mlg': Info(en: ['Malagasy']), + 'mlt': Info(en: ['Maltese']), + 'mnc': Info(en: ['Manchu']), + 'mni': Info(en: ['Manipuri']), + 'mno': Info(en: ['Manobo languages']), + 'moh': Info(en: ['Mohawk']), + 'mon': Info(en: ['Mongolian']), + 'mos': Info(en: ['Mossi']), + 'mri': Info(en: ['Maori']), + 'msa': Info(en: ['Malay']), + 'mul': Info(en: ['Multiple languages']), + 'mun': Info(en: ['Munda languages']), + 'mus': Info(en: ['Creek']), + 'mwl': Info(en: ['Mirandese']), + 'mwr': Info(en: ['Marwari']), + 'mya': Info(en: ['Burmese']), + 'myn': Info(en: ['Mayan languages']), + 'myv': Info(en: ['Erzya']), + 'nah': Info(en: ['Nahuatl languages']), + 'nai': Info(en: ['North American Indian languages']), + 'nap': Info(en: ['Neapolitan']), + 'nau': Info(en: ['Nauru']), + 'nav': Info(en: ['Navajo', 'Navaho']), + 'nbl': Info(en: ['Ndebele, South', 'South Ndebele']), + 'nde': Info(en: ['Ndebele, North', 'North Ndebele']), + 'ndo': Info(en: ['Ndonga']), + 'nds': Info(en: ['Low German', 'Low Saxon', 'German, Low', 'Saxon, Low']), + 'nep': Info(en: ['Nepali']), + 'new': Info(en: ['Nepal Bhasa', 'Newari']), + 'nia': Info(en: ['Nias']), + 'nic': Info(en: ['Niger-Kordofanian languages']), + 'niu': Info(en: ['Niuean']), + 'nld': Info(en: ['Dutch', 'Flemish']), + 'nno': Info(en: ['Norwegian Nynorsk', 'Nynorsk, Norwegian']), + 'nob': Info(en: ['Bokmål, Norwegian', 'Norwegian Bokmål']), + 'nog': Info(en: ['Nogai']), + 'non': Info(en: ['Norse, Old']), + 'nor': Info(en: ['Norwegian']), + 'nqo': Info(en: ['N\'Ko']), + 'nso': Info(en: ['Pedi', 'Sepedi', 'Northern Sotho']), + 'nub': Info(en: ['Nubian languages']), + 'nwc': Info(en: ['Classical Newari', 'Old Newari', 'Classical Nepal Bhasa']), + 'nya': Info(en: ['Chichewa', 'Chewa', 'Nyanja']), + 'nym': Info(en: ['Nyamwezi']), + 'nyn': Info(en: ['Nyankole']), + 'nyo': Info(en: ['Nyoro']), + 'nzi': Info(en: ['Nzima']), + 'oci': Info(en: ['Occitan (post 1500)']), + 'oji': Info(en: ['Ojibwa']), + 'ori': Info(en: ['Oriya']), + 'orm': Info(en: ['Oromo']), + 'osa': Info(en: ['Osage']), + 'oss': Info(en: ['Ossetian', 'Ossetic']), + 'ota': Info(en: ['Turkish, Ottoman (1500-1928)']), + 'oto': Info(en: ['Otomian languages']), + 'paa': Info(en: ['Papuan languages']), + 'pag': Info(en: ['Pangasinan']), + 'pal': Info(en: ['Pahlavi']), + 'pam': Info(en: ['Pampanga', 'Kapampangan']), + 'pan': Info(en: ['Panjabi', 'Punjabi']), + 'pap': Info(en: ['Papiamento']), + 'pau': Info(en: ['Palauan']), + 'peo': Info(en: ['Persian, Old (ca.600-400 B.C.)']), + 'per': Info(en: ['Persian']), + 'phi': Info(en: ['Philippine languages']), + 'phn': Info(en: ['Phoenician']), + 'pli': Info(en: ['Pali']), + 'pol': Info(en: ['Polish']), + 'pon': Info(en: ['Pohnpeian']), + 'por': Info(en: ['Portuguese']), + 'pra': Info(en: ['Prakrit languages']), + 'pro': Info(en: ['Provençal, Old (to 1500)', 'Occitan, Old (to 1500)']), + 'pus': Info(en: ['Pushto', 'Pashto']), + 'que': Info(en: ['Quechua']), + 'raj': Info(en: ['Rajasthani']), + 'rap': Info(en: ['Rapanui']), + 'rar': Info(en: ['Rarotongan', 'Cook Islands Maori']), + 'roa': Info(en: ['Romance languages']), + 'roh': Info(en: ['Romansh']), + 'rom': Info(en: ['Romany']), + 'ron': Info(en: ['Romanian', 'Moldavian', 'Moldovan']), + 'rum': Info(en: ['Romanian', 'Moldavian', 'Moldovan']), + 'run': Info(en: ['Rundi']), + 'rup': Info(en: ['Aromanian', 'Arumanian', 'Macedo-Romanian']), + 'rus': Info(en: ['Russian']), + 'sad': Info(en: ['Sandawe']), + 'sag': Info(en: ['Sango']), + 'sah': Info(en: ['Yakut']), + 'sai': Info(en: ['South American Indian languages']), + 'sal': Info(en: ['Salishan languages']), + 'sam': Info(en: ['Samaritan Aramaic']), + 'san': Info(en: ['Sanskrit']), + 'sas': Info(en: ['Sasak']), + 'sat': Info(en: ['Santali']), + 'scn': Info(en: ['Sicilian']), + 'sco': Info(en: ['Scots']), + 'sel': Info(en: ['Selkup']), + 'sem': Info(en: ['Semitic languages']), + 'sga': Info(en: ['Irish, Old (to 900)']), + 'sgn': Info(en: ['Sign Languages']), + 'shn': Info(en: ['Shan']), + 'sid': Info(en: ['Sidamo']), + 'sin': Info(en: ['Sinhala', 'Sinhalese']), + 'sio': Info(en: ['Siouan languages']), + 'sit': Info(en: ['Sino-Tibetan languages']), + 'sla': Info(en: ['Slavic languages']), + 'slk': Info(en: ['Slovak']), + 'slo': Info(en: ['Slovak']), + 'slv': Info(en: ['Slovenian']), + 'sma': Info(en: ['Southern Sami']), + 'sme': Info(en: ['Northern Sami']), + 'smi': Info(en: ['Sami languages']), + 'smj': Info(en: ['Lule Sami']), + 'smn': Info(en: ['Inari Sami']), + 'smo': Info(en: ['Samoan']), + 'sms': Info(en: ['Skolt Sami']), + 'sna': Info(en: ['Shona']), + 'snd': Info(en: ['Sindhi']), + 'snk': Info(en: ['Soninke']), + 'sog': Info(en: ['Sogdian']), + 'som': Info(en: ['Somali']), + 'son': Info(en: ['Songhai languages']), + 'sot': Info(en: ['Sotho, Southern']), + 'spa': Info(en: ['Spanish', 'Castilian']), + 'sqi': Info(en: ['Albanian']), + 'srd': Info(en: ['Sardinian']), + 'srn': Info(en: ['Sranan Tongo']), + 'srp': Info(en: ['Serbian']), + 'srr': Info(en: ['Serer']), + 'ssa': Info(en: ['Nilo-Saharan languages']), + 'ssw': Info(en: ['Swati']), + 'suk': Info(en: ['Sukuma']), + 'sun': Info(en: ['Sundanese']), + 'sus': Info(en: ['Susu']), + 'sux': Info(en: ['Sumerian']), + 'swa': Info(en: ['Swahili']), + 'swe': Info(en: ['Swedish']), + 'syc': Info(en: ['Classical Syriac']), + 'syr': Info(en: ['Syriac']), + 'tah': Info(en: ['Tahitian']), + 'tai': Info(en: ['Tai languages']), + 'tam': Info(en: ['Tamil']), + 'tat': Info(en: ['Tatar']), + 'tel': Info(en: ['Telugu']), + 'tem': Info(en: ['Timne']), + 'ter': Info(en: ['Tereno']), + 'tet': Info(en: ['Tetum']), + 'tgk': Info(en: ['Tajik']), + 'tgl': Info(en: ['Tagalog']), + 'tha': Info(en: ['Thai']), + 'tib': Info(en: ['Tibetan']), + 'tig': Info(en: ['Tigre']), + 'tir': Info(en: ['Tigrinya']), + 'tiv': Info(en: ['Tiv']), + 'tkl': Info(en: ['Tokelau']), + 'tlh': Info(en: ['Klingon', 'tlhIngan-Hol']), + 'tli': Info(en: ['Tlingit']), + 'tmh': Info(en: ['Tamashek']), + 'tog': Info(en: ['Tonga (Nyasa)']), + 'ton': Info(en: ['Tonga (Tonga Islands)']), + 'tpi': Info(en: ['Tok Pisin']), + 'tsi': Info(en: ['Tsimshian']), + 'tsn': Info(en: ['Tswana']), + 'tso': Info(en: ['Tsonga']), + 'tuk': Info(en: ['Turkmen']), + 'tum': Info(en: ['Tumbuka']), + 'tup': Info(en: ['Tupi languages']), + 'tur': Info(en: ['Turkish']), + 'tut': Info(en: ['Altaic languages']), + 'tvl': Info(en: ['Tuvalu']), + 'twi': Info(en: ['Twi']), + 'tyv': Info(en: ['Tuvinian']), + 'udm': Info(en: ['Udmurt']), + 'uga': Info(en: ['Ugaritic']), + 'uig': Info(en: ['Uighur', 'Uyghur']), + 'ukr': Info(en: ['Ukrainian']), + 'umb': Info(en: ['Umbundu']), + 'und': Info(en: ['Undetermined']), + 'urd': Info(en: ['Urdu']), + 'uzb': Info(en: ['Uzbek']), + 'vai': Info(en: ['Vai']), + 'ven': Info(en: ['Venda']), + 'vie': Info(en: ['Vietnamese']), + 'vol': Info(en: ['Volapük']), + 'vot': Info(en: ['Votic']), + 'wak': Info(en: ['Wakashan languages']), + 'wal': Info(en: ['Wolaitta', 'Wolaytta']), + 'war': Info(en: ['Waray']), + 'was': Info(en: ['Washo']), + 'wel': Info(en: ['Welsh']), + 'wen': Info(en: ['Sorbian languages']), + 'wln': Info(en: ['Walloon']), + 'wol': Info(en: ['Wolof']), + 'xal': Info(en: ['Kalmyk', 'Oirat']), + 'xho': Info(en: ['Xhosa']), + 'yao': Info(en: ['Yao']), + 'yap': Info(en: ['Yapese']), + 'yid': Info(en: ['Yiddish']), + 'yor': Info(en: ['Yoruba']), + 'ypk': Info(en: ['Yupik languages']), + 'zap': Info(en: ['Zapotec']), + 'zbl': Info(en: ['Blissymbols', 'Blissymbolics', 'Bliss']), + 'zen': Info(en: ['Zenaga']), + 'zgh': Info(en: ['Standard Moroccan Tamazight']), + 'zha': Info(en: ['Zhuang', 'Chuang']), + 'zho': Info(en: ['Chinese']), + 'znd': Info(en: ['Zande languages']), + 'zul': Info(en: ['Zulu']), + 'zun': Info(en: ['Zuni']), + 'zxx': Info(en: ['No linguistic content', 'Not applicable']), + 'zza': Info(en: ['Zaza', 'Dimili', 'Dimli', 'Kirdki', 'Kirmanjki', 'Zazaki']), +}; diff --git a/lib/pages/player/audio_tracks.dart b/lib/pages/player/audio_tracks.dart new file mode 100644 index 0000000..ec39ad3 --- /dev/null +++ b/lib/pages/player/audio_tracks.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:iris/hooks/use_player_core.dart'; +import 'package:iris/utils/get_localizations.dart'; +import 'package:iris/utils/get_subtitle_title.dart'; +import 'package:iris/utils/logger.dart'; +import 'package:media_kit/media_kit.dart'; + +class AudioTracks extends HookWidget { + const AudioTracks({super.key, required this.playerCore}); + + final PlayerCore playerCore; + + @override + Widget build(BuildContext context) { + final t = getLocalizations(context); + + final focusNode = useFocusNode(); + + useEffect(() { + focusNode.requestFocus(); + return () => focusNode.unfocus(); + }, []); + + return ListView( + children: [ + ...playerCore.audios.map( + (audio) => ListTile( + focusNode: playerCore.audio == audio ? focusNode : null, + title: Text( + audio == AudioTrack.auto() + ? t.auto + : audio == AudioTrack.no() + ? t.off + : getTrackTitle(audio), + style: playerCore.audio == audio + ? TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ) + : TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + onTap: () { + logger( + 'Set audio track: ${audio.title ?? audio.language ?? audio.id}'); + playerCore.player.setAudioTrack(audio); + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/player/control_bar.dart b/lib/pages/player/control_bar.dart index 2d09952..3f9b2da 100644 --- a/lib/pages/player/control_bar.dart +++ b/lib/pages/player/control_bar.dart @@ -6,7 +6,7 @@ import 'package:iris/hooks/use_player_controller.dart'; import 'package:iris/hooks/use_player_core.dart'; import 'package:iris/models/storages/local_storage.dart'; import 'package:iris/pages/player/control_bar_slider.dart'; -import 'package:iris/pages/player/subtitles.dart'; +import 'package:iris/pages/player/subtitle_and_audio_track.dart'; import 'package:iris/pages/settings/settings.dart'; import 'package:iris/store/use_play_queue_store.dart'; import 'package:iris/utils/get_localizations.dart'; @@ -14,7 +14,6 @@ import 'package:iris/pages/player/play_queue.dart'; import 'package:iris/utils/resize_window.dart'; import 'package:iris/pages/show_popup.dart'; import 'package:iris/pages/storages/storages.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:window_manager/window_manager.dart'; class ControlBar extends HookWidget { @@ -200,18 +199,16 @@ class ControlBar extends HookWidget { }, ), IconButton( - tooltip: '${t.subtitles} ( S )', - icon: Icon( - playerCore.subtitle == SubtitleTrack.no() - ? Icons.subtitles_off_rounded - : Icons.subtitles_rounded, + tooltip: '${t.subtitle_and_audio_track} ( S )', + icon: const Icon( + Icons.subtitles_rounded, size: 19, ), onPressed: () async { showControlForHover( showPopup( context: context, - child: Subtitles(playerCore: playerCore), + child: SubtitleAndAudioTrack(playerCore: playerCore), direction: PopupDirection.right, ), ); diff --git a/lib/pages/player/iris_player.dart b/lib/pages/player/iris_player.dart index 6990a36..b823f42 100644 --- a/lib/pages/player/iris_player.dart +++ b/lib/pages/player/iris_player.dart @@ -10,7 +10,7 @@ import 'package:iris/info.dart'; import 'package:iris/models/storages/local_storage.dart'; import 'package:iris/pages/player/control_bar_slider.dart'; import 'package:iris/pages/player/play_queue.dart'; -import 'package:iris/pages/player/subtitles.dart'; +import 'package:iris/pages/player/subtitle_and_audio_track.dart'; import 'package:iris/pages/settings/settings.dart'; import 'package:iris/pages/show_popup.dart'; import 'package:iris/pages/storages/storages.dart'; @@ -43,6 +43,7 @@ class IrisPlayer extends HookWidget { useEffect(() { () async { + player.setSubtitleTrack(SubtitleTrack.no()); if (Platform.isAndroid) { NativePlayer nativePlayer = player.platform as NativePlayer; @@ -269,7 +270,7 @@ class IrisPlayer extends HookWidget { showControlForHover( showPopup( context: context, - child: Subtitles(playerCore: playerCore), + child: SubtitleAndAudioTrack(playerCore: playerCore), direction: PopupDirection.right, ), ); diff --git a/lib/pages/player/subtitle_and_audio_track.dart b/lib/pages/player/subtitle_and_audio_track.dart new file mode 100644 index 0000000..510efe3 --- /dev/null +++ b/lib/pages/player/subtitle_and_audio_track.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:iris/hooks/use_player_core.dart'; +import 'package:iris/pages/player/audio_tracks.dart'; +import 'package:iris/pages/player/subtitles.dart'; +import 'package:iris/utils/get_localizations.dart'; + +class ITab { + final String title; + final Widget child; + + const ITab({ + required this.title, + required this.child, + }); +} + +class SubtitleAndAudioTrack extends HookWidget { + const SubtitleAndAudioTrack({super.key, required this.playerCore}); + + final PlayerCore playerCore; + + @override + Widget build(BuildContext context) { + final t = getLocalizations(context); + + List tabs = [ + ITab(title: t.subtitle, child: Subtitles(playerCore: playerCore)), + ITab(title: t.audio_track, child: AudioTracks(playerCore: playerCore)), + ]; + + final tabController = useTabController(initialLength: tabs.length); + + return Column( + children: [ + Expanded( + child: TabBarView( + controller: tabController, + children: tabs.map((e) => Card(child: e.child)).toList(), + ), + ), + Divider( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.25), + height: 0, + ), + Container( + padding: EdgeInsets.zero, + child: Container( + padding: const EdgeInsets.fromLTRB(0, 0, 4, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TabBar( + controller: tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + tabs: tabs.map((tab) => Tab(text: tab.title)).toList()), + const Spacer(), + IconButton( + tooltip: '${t.close} ( Escape )', + icon: const Icon(Icons.close_rounded), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/player/subtitles.dart b/lib/pages/player/subtitles.dart index d533312..5127858 100644 --- a/lib/pages/player/subtitles.dart +++ b/lib/pages/player/subtitles.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:iris/hooks/use_player_core.dart'; import 'package:iris/utils/get_localizations.dart'; +import 'package:iris/utils/get_subtitle_title.dart'; import 'package:media_kit/media_kit.dart'; class Subtitles extends HookWidget { @@ -14,101 +15,55 @@ class Subtitles extends HookWidget { @override Widget build(BuildContext context) { final t = getLocalizations(context); - final externalSubtitles = useMemoized( - () => [...playerCore.externalSubtitles]..removeWhere((subtitle) => - playerCore.subtitles.any((item) => item.title == subtitle.name)), - [playerCore.externalSubtitles, playerCore.subtitles]); - return Column( + final focusNode = useFocusNode(); + + useEffect(() { + focusNode.requestFocus(); + return () => focusNode.unfocus(); + }, []); + + return ListView( children: [ - Expanded( - child: Card( - child: ListView( - children: [ - ListTile( - autofocus: playerCore.subtitle == SubtitleTrack.no(), - title: Text( - t.off, - style: playerCore.subtitle == SubtitleTrack.no() - ? TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ) - : TextStyle( - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - onTap: () { - playerCore.player.setSubtitleTrack(SubtitleTrack.no()); - Navigator.of(context).pop(); - }, - ), - ...playerCore.subtitles.map( - (subtitle) => ListTile( - autofocus: playerCore.subtitle == subtitle, - title: Text( - '${subtitle.title ?? subtitle.language}', - style: playerCore.subtitle == subtitle - ? TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ) - : TextStyle( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), + ...playerCore.subtitles.map( + (subtitle) => ListTile( + focusNode: playerCore.subtitle == subtitle ? focusNode : null, + title: Text( + subtitle == SubtitleTrack.no() ? t.off : getTrackTitle(subtitle), + style: playerCore.subtitle == subtitle + ? TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ) + : TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - onTap: () { - playerCore.player.setSubtitleTrack(subtitle); - Navigator.of(context).pop(); - }, - ), - ), - ...externalSubtitles.map( - (subtitle) => ListTile( - title: Text( - subtitle.name, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - onTap: () { - log('Set external subtitle: ${subtitle.name}'); - playerCore.player.setSubtitleTrack( - SubtitleTrack.uri( - subtitle.uri, - title: subtitle.name, - ), - ); - Navigator.of(context).pop(); - }, - ), - ), - ], ), + onTap: () { + log('Set subtitle: ${subtitle.title ?? subtitle.language ?? subtitle.id}'); + playerCore.player.setSubtitleTrack(subtitle); + Navigator.of(context).pop(); + }, ), ), - Divider( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.25), - height: 0, - ), - Container( - padding: const EdgeInsets.fromLTRB(16, 4, 4, 4), - child: Row( - children: [ - Text( - t.subtitles, - style: const TextStyle(fontWeight: FontWeight.w500), + ...playerCore.externalSubtitles.map( + (subtitle) => ListTile( + title: Text( + getTrackLanguage(subtitle.name) ?? subtitle.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - const Spacer(), - IconButton( - tooltip: '${t.close} ( Escape )', - icon: const Icon(Icons.close_rounded), - onPressed: () => Navigator.of(context).pop(), - ), - ], + ), + onTap: () { + log('Set external subtitle: ${subtitle.name}'); + playerCore.player.setSubtitleTrack( + SubtitleTrack.uri( + subtitle.uri, + title: subtitle.name, + ), + ); + Navigator.of(context).pop(); + }, ), ), ], diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 5d406ee..de974b9 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -33,13 +33,15 @@ class About extends HookWidget { leading: Image.asset('assets/images/icon.png', width: 24, height: 24), title: const Text(INFO.title), - subtitle: const Text(INFO.description), + subtitle: Text(t.app_description), ), ListTile( leading: const Icon(Icons.info_rounded), title: Text(t.version), subtitle: Text( packageInfo.value != null ? packageInfo.value!.version : ''), + onTap: () => launchURL( + '${INFO.githubUrl}/releases/tag/v${packageInfo.value?.version}'), ), ListTile( leading: const Icon(Icons.update_rounded), diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 5c77c31..5a35cb0 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -6,11 +6,11 @@ import 'package:iris/pages/settings/libraries.dart'; import 'package:iris/pages/settings/play.dart'; import 'package:iris/utils/get_localizations.dart'; -class SettingsTab { +class ITab { final String title; final Widget child; - const SettingsTab({ + const ITab({ required this.title, required this.child, }); @@ -25,21 +25,21 @@ class Settings extends HookWidget { Widget build(BuildContext context) { final t = getLocalizations(context); - List settingsTabs = [ - SettingsTab(title: t.general, child: const General()), - SettingsTab(title: t.play, child: const Play()), - SettingsTab(title: t.about, child: const About()), - SettingsTab(title: t.libraries, child: const Libraries()), + List tabs = [ + ITab(title: t.general, child: const General()), + ITab(title: t.play, child: const Play()), + ITab(title: t.about, child: const About()), + ITab(title: t.libraries, child: const Libraries()), ]; - final tabController = useTabController(initialLength: settingsTabs.length); + final tabController = useTabController(initialLength: tabs.length); return Column( children: [ Expanded( child: TabBarView( controller: tabController, - children: settingsTabs.map((e) => Card(child: e.child)).toList(), + children: tabs.map((e) => Card(child: e.child)).toList(), ), ), Divider( @@ -58,7 +58,7 @@ class Settings extends HookWidget { isScrollable: true, tabAlignment: TabAlignment.start, dividerColor: Colors.transparent, - tabs: settingsTabs.map((e) => Tab(text: e.title)).toList()), + tabs: tabs.map((e) => Tab(text: e.title)).toList()), const Spacer(), IconButton( tooltip: '${t.close} ( Escape )', diff --git a/lib/pages/storages/storages.dart b/lib/pages/storages/storages.dart index 5b4e000..c9afd7e 100644 --- a/lib/pages/storages/storages.dart +++ b/lib/pages/storages/storages.dart @@ -12,6 +12,16 @@ import 'package:iris/pages/dialog/show_local_dialog.dart'; import 'package:iris/pages/dialog/show_webdav_dialog.dart'; import 'package:iris/pages/storages/storages_list.dart'; +class ITab { + final String title; + final Widget child; + + const ITab({ + required this.title, + required this.child, + }); +} + class Storages extends HookWidget { const Storages({super.key}); @@ -21,7 +31,12 @@ class Storages extends HookWidget { final currentStorage = useStorageStore().select(context, (state) => state.currentStorage); - final tabController = useTabController(initialLength: 2); + List tabs = [ + ITab(title: t.storages, child: const StoragesList()), + ITab(title: t.favorites, child: const FavoriteStoragesList()), + ]; + + final tabController = useTabController(initialLength: tabs.length); return currentStorage != null ? Files(storage: currentStorage) @@ -30,14 +45,7 @@ class Storages extends HookWidget { Expanded( child: TabBarView( controller: tabController, - children: const [ - Card( - child: StoragesList(), - ), - Card( - child: FavoriteStoragesList(), - ), - ], + children: tabs.map((tab) => Card(child: tab.child)).toList(), ), ), Divider( @@ -58,10 +66,7 @@ class Storages extends HookWidget { isScrollable: true, tabAlignment: TabAlignment.start, dividerColor: Colors.transparent, - tabs: [ - Tab(text: t.storages), - Tab(text: t.favorites), - ], + tabs: tabs.map((tab) => Tab(text: tab.title)).toList(), ), ), PopupMenuButton( diff --git a/lib/utils/get_subtitle_title.dart b/lib/utils/get_subtitle_title.dart new file mode 100644 index 0000000..94db3cc --- /dev/null +++ b/lib/utils/get_subtitle_title.dart @@ -0,0 +1,42 @@ +import 'package:iris/l10n/iso_639.dart'; +import 'package:media_kit/media_kit.dart'; + +String getTrackTitle(dynamic track) { + if (track is SubtitleTrack || track is AudioTrack) { + if (track.title != null) { + final lowerCaseTitle = track.title!.toLowerCase(); + final languageFromTitle = getTrackLanguage(lowerCaseTitle); + if (languageFromTitle != null) { + return languageFromTitle; + } + return track.title!; + } + + if (track.language != null) { + final lowerCaseLanguage = track.language!.toLowerCase(); + final languageFromLanguage = getTrackLanguage(lowerCaseLanguage); + if (languageFromLanguage != null) { + return languageFromLanguage; + } + return track.language!; + } + return track.id; + } + return ''; +} + +String? getTrackLanguage(String languageCode) { + if (customLanguageCodes[languageCode] != null) { + return '${(customLanguageCodes[languageCode]!.en).join(', ')}, $languageCode'; + } + + if (iso_639_1[languageCode] != null) { + return '${(iso_639_1[languageCode]!.en).join(', ')}, $languageCode'; + } + + if (iso_639_2[languageCode] != null) { + return '${(iso_639_2[languageCode]!.en).join(', ')}, $languageCode'; + } + + return null; +} diff --git a/pubspec.yaml b/pubspec.yaml index 7919a37..19376e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+1 +version: 1.0.2+1 environment: sdk: ^3.5.4 @@ -88,7 +88,6 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/fonts/ - assets/images/ # An image asset can refer to one or more resolution-specific "variants", see