* feat: built-in audio track switch

* update readme

* update subtitle icon and l10n

* fix: delay setting default audio track

* move android subtitle font

* improved subtitle and audio track title display

* v1.0.2
This commit is contained in:
22
2025-01-12 20:21:10 +08:00
committed by GitHub
parent fdbc977562
commit 8bb19aaf7d
22 changed files with 1211 additions and 289 deletions

View File

@@ -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: |

View File

@@ -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 版本支持自动更新
* Windows 版本支持自动更新
## v1.0.0
### Changelog
* Supports WebDAV and local storage video playback
### 更新日志
* 支持 WebDAV 和本地存储视频播放

View File

@@ -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

View File

@@ -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)
## 贡献

35
extract_log.py Normal file
View File

@@ -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 <version>")
sys.exit(1)
version = sys.argv[1]
extract_log(version)

View File

@@ -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<void> play() async {
await useAppStore().updateAutoPlay(true);

View File

@@ -14,12 +14,12 @@ import 'package:media_kit/media_kit.dart';
class PlayerCore {
final Player player;
final String title;
final List<FileItem> playQueue;
final int currentIndex;
final FileItem? currentFile;
final SubtitleTrack subtitle;
final List<SubtitleTrack> subtitles;
final List<Subtitle> externalSubtitles;
final AudioTrack audio;
final List<AudioTrack> 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<AudioTrack> audios =
useMemoized(() => (tracks?.audio ?? []), [tracks?.audio]);
List<SubtitleTrack> subtitles = useMemoized(
() => [...(tracks?.subtitle ?? [])]
..removeWhere((subtitle) => subtitle == SubtitleTrack.auto()),
[tracks?.subtitle]);
final List<Subtitle>? 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<List<SubtitleTrack>>([]);
final List<Subtitle>? 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,

View File

@@ -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';

View File

@@ -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"
}

View File

@@ -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": "版本"
}

720
lib/l10n/iso_639.dart Normal file
View File

@@ -0,0 +1,720 @@
class Info {
final List<String> en;
Info({required this.en});
}
Map<String, Info> customLanguageCodes = {
'chs': Info(en: ['Chinese (Simplified)']),
'cht': Info(en: ['Chinese (Traditional)']),
};
Map<String, Info> 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<String, Info> 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']),
};

View File

@@ -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();
},
),
),
],
);
}
}

View File

@@ -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,
),
);

View File

@@ -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,
),
);

View File

@@ -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<ITab> 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(),
),
],
),
),
),
],
);
}
}

View File

@@ -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();
},
),
),
],

View File

@@ -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),

View File

@@ -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<SettingsTab> 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<ITab> 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 )',

View File

@@ -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<ITab> 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<String>(

View File

@@ -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;
}

View File

@@ -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