261 Commits

Author SHA1 Message Date
Shikin Y. User a9b86c1667 Update iOS library [CI/CD] 2024-01-10 16:12:06 +03:00
Jura Shikin 15b3028bce Merge branch 'feature/PSDK-1040/ru_locale' into 'develop'
PSDK-1040 - Русская локаль

See merge request mobile/Flutter/nut_player!106
2024-01-09 18:58:02 +03:00
Elena Nazarova 76f3ddb9ea PSDK-1040 - Русская локаль 2024-01-09 18:58:02 +03:00
Shikin Y. User 77113197a9 Update iOS library [CI/CD] 2024-01-09 13:23:05 +03:00
Shikin Y. User 872762bb3f Update iOS library [CI/CD] 2023-12-29 12:22:26 +03:00
Jura Shikin 95908a72f4 Merge branch 'feature/PSDK-1017/player_version' into 'develop'
PSDK-1017 - Версия плеера

See merge request mobile/Flutter/nut_player!105
2023-12-29 12:15:08 +03:00
Elena Nazarova 7b3a76233a PSDK-1017 - Версия плеера 2023-12-29 12:15:07 +03:00
Shikin Y. User 5d9c5b8fcf Update iOS library [CI/CD] 2023-12-28 12:38:15 +03:00
Shikin Y. User dc85ce231a Update iOS library [CI/CD] 2023-12-27 15:12:03 +03:00
Shikin Y. User c79f69073b Update iOS library [CI/CD] 2023-12-27 12:44:11 +03:00
Андрей Геращенко ab7a54b7c5 Merge branch 'feature/PSDK-1173/end' into 'develop'
PSDK-1173 - Завершение видео

See merge request mobile/Flutter/nut_player!104
2023-12-27 12:00:41 +03:00
Elena Nazarova 9e958f1d36 PSDK-1173 - Завершение видео 2023-12-27 12:00:41 +03:00
Jura Shikin a9160da8cb Merge branch 'feature/PSDK-1040/rus_locale' into 'develop'
PSDK-1040 - Русская локализация внутри флаттер-приложения

See merge request mobile/Flutter/nut_player!102
2023-12-25 16:12:46 +03:00
Elena Nazarova b41357c3c4 PSDK-1040 - Русская локализация внутри флаттер-приложения 2023-12-25 16:12:46 +03:00
Jura Shikin 97a8fcf335 Merge branch 'feature/PSDK-1018/stop_button_corrections' into 'develop'
PSDK-1018 - Правка верстки

See merge request mobile/Flutter/nut_player!103
2023-12-25 16:09:37 +03:00
Elena Nazarova ffb022fcfa PSDK-1018 - Правка верстки 2023-12-25 16:09:36 +03:00
Shikin Y. User 6e52bc2dfd Update iOS library [CI/CD] 2023-12-22 11:43:57 +03:00
Shikin Y. User ccade48858 Update iOS library [CI/CD] 2023-12-22 11:39:57 +03:00
Jura Shikin 387d453ccb Merge branch 'feature/PSDK-1018/stop_button' into 'develop'
PSDK-1018 - Изменила заголовок

See merge request mobile/Flutter/nut_player!101
2023-12-22 11:12:34 +03:00
Elena Nazarova 4e4ee0d099 PSDK-1018 - Изменила заголовок 2023-12-22 11:12:34 +03:00
Андрей Геращенко 67d12b034c Merge branch 'feature/PSDK-1133/fix_pip_arg' into 'develop'
PSDK-1133 - Исправил опечатку

See merge request mobile/Flutter/nut_player!100
2023-12-19 22:11:22 +03:00
Андрей Геращенко b8daeb904b PSDK-1133 - Исправил опечатку 2023-12-19 22:08:31 +03:00
Андрей Геращенко 8223afa372 Merge branch 'feature/PSDK-1133/fix_pip_2' into 'develop'
PSDK-1133 - Исправлена работа с параметром isLive

See merge request mobile/Flutter/nut_player!99
2023-12-18 17:31:31 +03:00
Андрей Геращенко 9af7d065ec PSDK-1133 - Исправлена работа с параметром isLive 2023-12-18 17:31:31 +03:00
Shikin Y. User d8a3800e2b Update iOS library [CI/CD] 2023-12-18 14:57:44 +03:00
Jura Shikin 5f60751d10 Merge branch 'feature/PSDK-1162/long_listtile_value' into 'develop'
PSDK-1162 - Правка верстки при длинном значении ячейки

See merge request mobile/Flutter/nut_player!97
2023-12-14 19:14:31 +03:00
Elena Nazarova 5c93e4a032 PSDK-1162 - Правка верстки при длинном значении ячейки 2023-12-14 19:14:30 +03:00
Jura Shikin 5c3385bc6b Merge branch 'feature/PSDK-1157/qualities' into 'develop'
PSDK-1157 - Смена качеств из скина

See merge request mobile/Flutter/nut_player!96
2023-12-12 18:45:09 +03:00
Elena Nazarova a57268c778 PSDK-1157 - Смена качеств из скина 2023-12-12 18:45:08 +03:00
Shikin Y. User 9d607dadf6 Update iOS library [CI/CD] 2023-12-12 17:26:28 +03:00
Jura Shikin 31e27434a9 Merge branch 'feature/PSDK-1160/quality_typo' into 'develop'
PSDK-1160 - Убрала запятую

See merge request mobile/Flutter/nut_player!95
2023-12-12 12:25:02 +03:00
Elena Nazarova 7df0d1b9ca PSDK-1160 - Убрала запятую 2023-12-12 12:25:02 +03:00
Андрей Геращенко af40337fc1 Merge branch 'feature/PSDK-1155/json_api' into 'develop'
PSDK-1155 - Поддержка json-провайдера

See merge request mobile/Flutter/nut_player!94
2023-12-11 18:49:15 +03:00
Elena Nazarova 6e5294e3c3 PSDK-1155 - Поддержка json-провайдера 2023-12-11 18:49:15 +03:00
Shikin Y. User 2835c49184 Update iOS library [CI/CD] 2023-12-11 10:56:42 +03:00
Shikin Y. User 0fa0e04150 Update iOS library [CI/CD] 2023-12-11 10:51:01 +03:00
Jura Shikin f2ae773d71 Merge branch 'feature/PSDK-1153/settings_subs_corrections' into 'develop'
PSDK-1153 - Правка экрана настроек и вывода субтитров

See merge request mobile/Flutter/nut_player!93
2023-12-08 22:03:07 +03:00
Elena Nazarova e52362282f PSDK-1153 - Правка экрана настроек и вывода субтитров 2023-12-08 22:03:06 +03:00
Андрей Геращенко 92d1a5801f Merge branch 'feature/PSDK-1038/timeline_slider' into 'develop'
PSDK-1038 - Перенесла скролл

See merge request mobile/Flutter/nut_player!92
2023-12-08 13:24:45 +03:00
Elena Nazarova 914c011e02 PSDK-1038 - Перенесла скролл 2023-12-08 13:24:41 +03:00
Jura Shikin 33141b40d1 Merge branch 'feature/PSDK-1150/save_content' into 'develop'
PSDK-1150 - Правки для сохранения выбранного контента

See merge request mobile/Flutter/nut_player!91
2023-12-07 19:43:33 +03:00
Elena Nazarova c1cd13e674 PSDK-1150 - Правки для сохранения выбранного контента 2023-12-07 19:43:32 +03:00
Shikin Y. User f6f2f3cb7e Update iOS library [CI/CD] 2023-12-07 16:41:04 +03:00
Андрей Геращенко 7dff0bf671 Merge branch 'feature/PSDK-1131/skin_correction' into 'develop'
PSDK-1131 - Небольшая правка скина

See merge request mobile/Flutter/nut_player!90
2023-12-07 10:46:31 +03:00
Elena Nazarova cef78d59dc PSDK-1131 - Небольшая правка скина 2023-12-07 10:46:30 +03:00
Jura Shikin 9dce72e693 Merge branch 'feature/PSDK-1151/fix_colour' into 'develop'
PSDK-1151 - Поправил отображение темы

See merge request mobile/Flutter/nut_player!89
2023-12-06 18:11:52 +03:00
Андрей Геращенко 303d3731c3 PSDK-1151 - Поправил отображение темы 2023-12-06 18:03:01 +03:00
Андрей Геращенко a6587d3fd5 Merge branch 'feature/PSDK-1152/autostart' into 'develop'
PSDK-1152 - Правка автостарта

See merge request mobile/Flutter/nut_player!88
2023-12-06 17:19:35 +03:00
Elena Nazarova f68dce0fd8 PSDK-1152 - Правка автостарта 2023-12-06 17:19:34 +03:00
Shikin Y. User c5158de172 Update iOS library [CI/CD] 2023-12-05 19:27:52 +03:00
Juraldinio 99f23ad4b5 Disable build confrontare 2023-12-05 18:48:08 +03:00
Elena Nazarova 277736a437 пробел для инициирования сборки 2023-12-05 18:45:58 +03:00
Jura Shikin 4e353591ad Merge branch 'feature/PSDK-1151/blue_buttons' into 'develop'
PSDK-1151 - Правка активного цвета кнопок

See merge request mobile/Flutter/nut_player!87
2023-12-05 17:10:31 +03:00
Elena Nazarova 118472fee2 PSDK-1151 - Правка активного цвета кнопок 2023-12-05 17:10:31 +03:00
Jura Shikin 40d24b600f Merge branch 'feature/PSDK-1137/naming_policy' into 'develop'
PSDK-1137 - Добавлены настройки смены названий качеств

See merge request mobile/Flutter/nut_player!85
2023-12-04 21:22:41 +03:00
Андрей Геращенко fdd9279c31 PSDK-1137 - Добавлены настройки смены названий качеств 2023-12-04 21:22:41 +03:00
Jura Shikin eef04bec28 Merge branch 'feature/PSDK-1148/speed_corrections' into 'develop'
PSDK-1148 - Правка установки скорости

See merge request mobile/Flutter/nut_player!84
2023-12-04 10:47:01 +03:00
Elena Nazarova 400f1807ba PSDK-1148 - Правка установки скорости 2023-12-04 10:46:59 +03:00
Андрей Геращенко 306843379a Merge branch 'feature/PSDK-1140/settings' into 'develop'
PSDK-1140 - Настройки

See merge request mobile/Flutter/nut_player!82
2023-12-01 17:43:02 +03:00
Elena Nazarova e58b10f27a PSDK-1140 - Настройки 2023-12-01 17:43:02 +03:00
Elena Nazarova 2cc84fb3cf Merge branch 'feature/PSDK-1137/receive_qualities' into 'develop'
PSDK-1137 - Добавил переключение качеств и поддержку стартового качества

See merge request mobile/Flutter/nut_player!83
2023-12-01 17:36:16 +03:00
Андрей Геращенко 833a478637 PSDK-1137 - Добавил переключение качеств и поддержку стартового качества 2023-12-01 17:36:16 +03:00
Андрей Геращенко ca6c2505ba Merge branch 'feature/PSDK-1140/settings' into 'develop'
PSDK-1140 - Верстка экрана настроек

See merge request mobile/Flutter/nut_player!80
2023-11-30 12:09:39 +03:00
Elena Nazarova b2d8872735 PSDK-1140 - Верстка экрана настроек 2023-11-30 12:09:39 +03:00
Elena Nazarova da10b69931 Merge branch 'feature/PSDK-1137/receive_qualities' into 'develop'
PSDK-1137 - Получил качества

See merge request mobile/Flutter/nut_player!81
2023-11-30 12:07:31 +03:00
Андрей Геращенко 56f61be4bb PSDK-1137 - Получил качества 2023-11-30 12:07:31 +03:00
Elena Nazarova 60fdf1df73 Merge branch 'feature/PSDK-1149/enablepip_by_default' into 'develop'
PSDK-1149 - Активировал режим картинка-в-картинке по умолчанию

See merge request mobile/Flutter/nut_player!78
2023-11-29 13:16:24 +03:00
Elena Nazarova 6be8b44149 Merge branch 'feature/PSDK-1138/start_quality' into 'develop'
PSDK-1138 - Стартовое качество

See merge request mobile/Flutter/nut_player!76
2023-11-29 13:16:15 +03:00
Андрей Геращенко 69b10f1285 PSDK-1138 - Стартовое качество 2023-11-29 13:16:14 +03:00
Jura Shikin 2127c8442c Merge branch 'feature/delete_derived' into 'develop'
Добавлено удаление DerivedData

See merge request mobile/Flutter/nut_player!79
2023-11-29 12:29:41 +03:00
Андрей Геращенко 358b60dc58 Добавлено удаление DerivedData 2023-11-29 12:21:53 +03:00
Андрей Геращенко 69e4e62231 PSDK-1149 - Активировал режим картинка-в-картинке по умолчанию 2023-11-29 11:13:26 +03:00
Jura Shikin 591d3ae284 Merge branch 'feature/PSDK-1142/fb_crash' into 'develop'
PSDK-1142 - Падение

See merge request mobile/Flutter/nut_player!77
2023-11-29 11:10:06 +03:00
Elena Nazarova db11344284 PSDK-1142 - Падение 2023-11-29 11:10:06 +03:00
Андрей Геращенко 58737dd089 Merge branch 'feature/PSDK-1141/log_corrections' into 'develop'
PSDK-1141 - Отрисовка лога только когда он включен

See merge request mobile/Flutter/nut_player!75
2023-11-28 15:17:09 +03:00
Elena Nazarova 30c6758b1e PSDK-1141 - Отрисовка лога только когда он включен 2023-11-28 15:17:09 +03:00
Shikin Y. User 7d67135987 Update iOS library [CI/CD] 2023-11-28 13:38:18 +03:00
Elena Nazarova 576b2303f2 Merge branch 'feature/PSDK-1144/fullscreen_orientation' into 'develop'
PSDK-1144 - Добавил повороты в полноэкранном режиме

See merge request mobile/Flutter/nut_player!72
2023-11-28 13:27:49 +03:00
Андрей Геращенко c4b503a9c8 PSDK-1144 - Добавил повороты в полноэкранном режиме 2023-11-28 13:27:49 +03:00
Elena Nazarova 0b0b7e5c7e Merge branch 'feature/PSDK-1133/fix_pip_background' into 'develop'
PSDK-1133 - Исправления фонового режима для картинки-в-картинке

See merge request mobile/Flutter/nut_player!74
2023-11-28 13:25:59 +03:00
Андрей Геращенко df2912ad9c PSDK-1133 - Исправления фонового режима для картинки-в-картинке 2023-11-28 12:59:54 +03:00
Андрей Геращенко fd4e69caaf Merge branch 'feature/PSDK-1141/log_levels' into 'develop'
PSDK-1141 - Уровни логирования

See merge request mobile/Flutter/nut_player!71
2023-11-27 15:26:13 +03:00
Elena Nazarova d0ef98cafe PSDK-1141 - Уровни логирования 2023-11-27 15:26:12 +03:00
Андрей Геращенко d63df8ad06 Merge branch 'feature/PSDK-1132/fullscreen' into 'develop'
PSDK-1132 - Добавлена поддержка полноэкранного режима

See merge request mobile/Flutter/nut_player!69
2023-11-24 12:59:39 +03:00
Андрей Геращенко abb6f0f208 PSDK-1132 - Добавлена поддержка полноэкранного режима 2023-11-24 12:59:39 +03:00
Jura Shikin 8258729684 Merge branch 'feature/PSDK-1139/subs' into 'develop'
PSDK-1139 - Субтитры

See merge request mobile/Flutter/nut_player!70
2023-11-23 21:17:25 +03:00
Elena Nazarova 9b63e0bcfd PSDK-1139 - Субтитры 2023-11-23 21:17:25 +03:00
Juraldinio 3ca84c7c24 fix build problem 2023-11-23 16:52:08 +03:00
Jura Shikin ab08b7fc6a Merge branch 'feature/PSDK-1130/start' into 'develop'
PSDK-1130 - Правки по стартовой позиции

See merge request mobile/Flutter/nut_player!68
2023-11-21 14:19:06 +03:00
Elena Nazarova ccacc43b0e PSDK-1130 - Правки по стартовой позиции 2023-11-21 14:19:06 +03:00
Андрей Геращенко 03809c18ce Merge branch 'feature/PSDK-1131/skin' into 'develop'
PSDK-1131 - Внешний скин

See merge request mobile/Flutter/nut_player!67
2023-11-21 10:50:51 +03:00
Elena Nazarova 90a366d4bb PSDK-1131 - Внешний скин 2023-11-21 10:50:51 +03:00
Shikin Y. User a411d071ac Update iOS library [CI/CD] 2023-11-16 18:40:45 +03:00
Elena Nazarova a4b9a00955 Merge branch 'feature/PSDK-1134/settings' into 'develop'
PSDK-1134 - Переключение настроек

See merge request mobile/Flutter/nut_player!66
2023-11-16 13:41:16 +03:00
Андрей Геращенко 03562349cc PSDK-1134 - Переключение настроек 2023-11-16 13:41:16 +03:00
Jura Shikin b81385a069 Merge branch 'feature/PSDK-1130/startPosition' into 'develop'
PSDK-1130 - Стартовая позиция

See merge request mobile/Flutter/nut_player!65
2023-11-16 12:41:37 +03:00
Elena Nazarova 1c9541306f PSDK-1130 - Стартовая позиция 2023-11-16 12:41:36 +03:00
Elena Nazarova 6b81a92ddc Merge branch 'feature/PSDK-1135/brightness' into 'develop'
PSDK-1135 - Правки смены яркости

See merge request mobile/Flutter/nut_player!64
2023-11-16 12:18:46 +03:00
Андрей Геращенко 645eede99d PSDK-1135 - Правки смены яркости 2023-11-16 12:18:44 +03:00
Elena Nazarova 6c2fde9513 Merge branch 'feature/PSDK-1133/pip' into 'develop'
PSDK-1133 - Подключено переключение настройки картинка в картинке

See merge request mobile/Flutter/nut_player!62
2023-11-15 20:10:15 +03:00
Jura Shikin 863b0bb2ed Merge branch 'feature/PSDK-1029/timeouts_correction' into 'develop'
PSDK-1029 - Не сохранялись задержки

See merge request mobile/Flutter/nut_player!61
2023-11-14 17:14:22 +03:00
Elena Nazarova 4f01af6d90 PSDK-1029 - Не сохранялись задержки 2023-11-14 17:14:22 +03:00
Андрей Геращенко 6c27b8e33e PSDK-1133 - Подключено переключение настройки картинка в картинке 2023-11-14 17:03:59 +03:00
Андрей Геращенко 3a186c2380 Merge branch 'feature/PSDK-1129/autostart_loop' into 'develop'
PSDK-1129 - Прокинуты автостарт, зацикливание

See merge request mobile/Flutter/nut_player!60
2023-11-14 11:12:08 +03:00
Elena Nazarova 9ce0d7909b PSDK-1129 - Прокинуты автостарт, зацикливание 2023-11-14 11:12:07 +03:00
Shikin Y. User 5aa0d814aa Update iOS library [CI/CD] 2023-11-13 20:59:28 +03:00
Jura Shikin 8f78121b6a Merge branch 'feature/PSDK-1113/fix_error_message_naming' into 'develop'
PSDK-1113 Fix error message

See merge request mobile/Flutter/nut_player!59
2023-11-13 14:38:10 +03:00
Николай Коротков 5a5cdc7a3b PSDK-1113 Fix error message 2023-11-13 14:38:09 +03:00
Shikin Y. User 998b59fc26 Update iOS library [CI/CD] 2023-11-13 13:54:27 +03:00
Shikin Y. User 3b309987cc Update Android library [CI/CD] 2023-11-13 13:41:27 +03:00
Juraldinio f6c9f33759 Add show ErrorPlugin 2023-11-10 09:47:32 +03:00
Juraldinio b01dcdc14f Remove hideout player on error 2023-11-09 18:19:18 +03:00
Shikin Y. User b8477b6fcc Update Android library [CI/CD] 2023-11-09 18:03:37 +03:00
Jura Shikin 39ea894911 Merge branch 'feature/PSDK-1029/timeouts' into 'develop'
PSDK-1029 - Задержки

See merge request mobile/Flutter/nut_player!57
2023-11-09 13:40:20 +03:00
Elena Nazarova d8810095ac PSDK-1029 - Задержки 2023-11-09 13:40:20 +03:00
Juraldinio a8e47899fc PSDK-1128 - fix crush and update depenencies 2023-11-09 13:25:26 +03:00
Shikin Y. User 4ade88a340 Update Android library [CI/CD] 2023-11-09 12:54:40 +03:00
Shikin Y. User bf086be36c Update iOS library [CI/CD] 2023-11-09 11:34:48 +03:00
Shikin Y. User 39657f4c86 Update iOS library [CI/CD] 2023-11-09 11:31:02 +03:00
Juraldinio 1b36d750c5 Change name for update libraries job 2023-11-08 23:16:07 +03:00
Juraldinio c91621c095 Add handle automatically update android library 2023-11-08 23:03:24 +03:00
Jura Shikin e21f11a793 Merge branch 'feature/PSDK-1113/add_cause_and_name_for_error_message' into 'develop'
PSDK-1113 Add cause and name for error message

See merge request mobile/Flutter/nut_player!58
2023-11-08 22:27:11 +03:00
Николай Коротков 7d323b1a74 PSDK-1113 Add cause and name for error message 2023-11-08 22:27:11 +03:00
Juraldinio 316d55affd Update AAR for nut_player android 2023-11-08 14:39:59 +03:00
Shikin Y. User 1893e6a915 Update iOS library [CI/CD] 2023-11-07 17:09:15 +03:00
Shikin Y. User a88e02de31 Update iOS library [CI/CD] 2023-11-07 16:48:49 +03:00
Андрей Геращенко 3ca470f2bc Merge branch 'feature/PSDK-1095/fix_app_switch' into 'develop'
PSDK-1095 - Правки для сборки проекта

See merge request mobile/Flutter/nut_player!56
2023-11-03 11:55:04 +03:00
Андрей Геращенко 79a8b11ca4 PSDK-1095 - Правки для сборки проекта 2023-11-03 11:44:54 +03:00
Shikin Y. User 3a4b783d0b Update iOS library [CI/CD] 2023-11-03 10:53:29 +03:00
Juraldinio 1be9b03ed9 Update android library with new exoplayer 2023-11-02 10:25:49 +03:00
Elena Nazarova aa6eda389e Merge branch 'feature/PSDK-1020/fix_volume' into 'develop'
PSDK-1020: Исправлены установки звука

See merge request mobile/Flutter/nut_player!54
2023-10-31 19:49:53 +03:00
Андрей Геращенко 0afeb548c8 PSDK-1020: Исправлены установки звука 2023-10-31 19:49:53 +03:00
Juraldinio 641ab5d5a2 Allow intercept requests 2023-10-31 11:22:52 +03:00
Juraldinio 730d54a1da Manual task for trigger 2023-10-31 09:28:56 +03:00
Juraldinio e1379ff0d9 Remove minify and shrink 2023-10-30 22:17:11 +03:00
Jura Shikin e236fe641c Merge branch 'feature/autoupdate' into 'develop'
Feature/autoupdate

See merge request mobile/Flutter/nut_player!53
2023-10-30 22:12:47 +03:00
Jura Shikin c50b1f9ead Feature/autoupdate 2023-10-30 22:12:47 +03:00
Jura Shikin ca3b52190f Merge branch 'feature/auto_merge' into 'develop'
Feature/auto merge

See merge request mobile/Flutter/nut_player!52
2023-10-30 17:14:35 +03:00
Jura Shikin 672c4930e7 Feature/auto merge 2023-10-30 17:14:35 +03:00
Jura Shikin ca85dc91cc Disable skin for player 2023-10-30 15:49:41 +03:00
Jura Shikin c1c077b90b Add --no-shrink options to build android 2023-10-30 15:29:26 +03:00
Juraldinio 4fb4f23f3f Remove build stopper 2023-10-30 14:09:29 +03:00
Juraldinio 7fb17a36b4 Few corrections CI_PIPELINE_SOURCE 2023-10-30 12:44:30 +03:00
Juraldinio a1d32a9060 Echo CI_PIPELINE_SOURCE for debug 2023-10-30 12:41:40 +03:00
Juraldinio ec6e9ab536 Update flutter for passthrough autostart option 2023-10-28 12:00:57 +03:00
Juraldinio 2803ea99b7 Merge branch 'develop' of gitlab.nut.team:mobile/Flutter/nut_player into develop 2023-10-27 23:14:31 +03:00
Juraldinio eac47f5b38 PSDK-1085 - restore autostart and corrected live streaming 2023-10-27 23:13:40 +03:00
Jura Shikin 180830b694 Merge branch 'feature/PSDK-1080/fixed_splashing' into 'develop'
PSDK-1080: Убрал схлопывание проигрывателя при ошибках

See merge request mobile/Flutter/nut_player!51
2023-10-27 17:35:11 +03:00
Андрей Геращенко 15effa1df3 PSDK-1080: Убрал схлопывание проигрывателя при ошибках 2023-10-27 17:35:11 +03:00
Juraldinio f4d176ae7a Update framework 2023-10-27 12:34:56 +03:00
Juraldinio 836c94e9fb Optional named param 2023-10-27 10:20:50 +03:00
Juraldinio 6da3a02280 Fix decoding FlutterContentType with various types 2023-10-27 10:11:29 +03:00
Jura Shikin ce5b8251a3 Merge branch 'feature/PSDK-1081/fast_start' into 'develop'
PSDK-1081 Прокинуты настройки быстрого старта

See merge request mobile/Flutter/nut_player!49
2023-10-26 21:44:04 +03:00
Андрей Геращенко 7928ebe45d PSDK-1081 Прокинуты настройки быстрого старта 2023-10-26 21:44:04 +03:00
Jura Shikin b660ac2a86 Merge branch 'feature/PSDK-1016/add_visual_plugins' into 'develop'
PSDK-1016 Add visual plugins

See merge request mobile/Flutter/nut_player!48
2023-10-26 11:13:16 +03:00
Николай Коротков 799fde61cf PSDK-1016 Add visual plugins 2023-10-26 11:13:16 +03:00
Juraldinio ae978837ec Update framework 2023-10-26 10:52:48 +03:00
Андрей Геращенко 913190d8bc Merge branch 'feature/remove_unnecessary' into 'develop'
Убрал лишнее

See merge request mobile/Flutter/nut_player!47
2023-10-25 21:30:09 +03:00
Андрей Геращенко 82c3f27c33 Убрал лишнее 2023-10-25 18:24:30 +03:00
Андрей Геращенко f178b33df9 Merge branch 'feature/update-framework' into 'develop'
Обновил NutPlayer

See merge request mobile/Flutter/nut_player!46
2023-10-25 18:11:56 +03:00
Андрей Геращенко fbda0b09a0 Обновил NutPlayer 2023-10-25 17:57:42 +03:00
Jura Shikin 766c2cfd76 Merge branch 'feature/PSDK-1048/log_localization' into 'develop'
PSDK-1048 - Локализация лога для вывода качеств

See merge request mobile/Flutter/nut_player!45
2023-10-25 14:12:26 +03:00
Андрей Геращенко 78703aa2c3 PSDK-1048 - Локализация лога для вывода качеств 2023-10-25 14:12:26 +03:00
Juraldinio 05f73fd37d PSDK-1058 - known issue in apple for localization 2023-10-25 10:42:09 +03:00
Juraldinio a7ec09aa62 PSDK-1058 - add localization for log 2023-10-25 09:55:23 +03:00
Juraldinio 8204cae8c5 Fix flow for Flutter 2023-10-23 14:03:07 +03:00
Juraldinio 32deef424c Update events for compare with default player 2023-10-23 12:57:00 +03:00
Juraldinio 58c46ab346 Merge branch 'develop' of gitlab.nut.team:mobile/Flutter/nut_player into develop 2023-10-23 11:23:55 +03:00
Juraldinio 4035f4bab0 Update compiled framework for iOS 2023-10-23 11:23:17 +03:00
Juraldinio bce1efc309 Visibility for player icons by params from initialize method 2023-10-22 20:30:28 +03:00
Juraldinio 2d48e2148a Remove skin icons pip and fullscreen by default 2023-10-22 12:31:51 +03:00
Juraldinio 8fb9cd8572 PSDK-1030 - Remove space and log format(milliseconds) 2023-10-21 22:35:50 +03:00
jsjardineiro 6389d37053 Add broadcast to android version 2023-10-20 21:07:29 +03:00
Jura Shikin 27ea2e8436 Merge branch 'feature/PSDK-976/add_player_content_from_dart_deserializer' into 'develop'
PSDK-976 Add deserializer to player content from dart

See merge request mobile/Flutter/nut_player!38
2023-10-18 21:12:37 +03:00
Николай Коротков 533fe0b4b1 PSDK-976 Add deserializer to player content from dart 2023-10-18 21:12:37 +03:00
Juraldinio 1701cb5400 noissue - Update framework 2023-10-17 17:13:49 +03:00
Juraldinio d4864e48f7 noissue - Fix show errors in player 2023-10-17 14:03:56 +03:00
Jura Shikin 6c4ef795a7 Merge branch 'feature/PSDK-1017/version' into 'develop'
PSDK-1017 - Версия (только приложения)

See merge request mobile/Flutter/nut_player!44
2023-10-13 18:41:14 +03:00
Elena Nazarova dd38843002 PSDK-1017 - Версия (только приложения) 2023-10-13 18:41:14 +03:00
Jura Shikin f8c24c071b Merge branch 'feature/PSDK-1024/alert_sheet' into 'develop'
PSDK-1024 - Правки по диалоговому окну

See merge request mobile/Flutter/nut_player!43
2023-10-13 17:57:32 +03:00
Elena Nazarova 7d5b876bda PSDK-1024 - Правки по диалоговому окну 2023-10-13 17:57:32 +03:00
Juraldinio 4d31c24998 noissue - revert hardcoded link 2023-10-13 11:47:35 +03:00
Juraldinio 130ba3a1ef noissue - hardcoded link 2023-10-13 11:47:11 +03:00
Juraldinio cfcecd3403 noissue - Allow pip 2023-10-13 11:24:11 +03:00
Juraldinio 2e859291e8 noissue - Update framework 2023-10-13 11:22:00 +03:00
Juraldinio 168a5044bd noissue - fullscreen icon show 2023-10-13 10:07:38 +03:00
Juraldinio d5d8ebc328 noisue - try fix deploy to apple 2023-10-12 19:29:48 +03:00
Jura Shikin 899efd763c Merge branch 'feature/PSDK-1036/settings' into 'develop'
PSDK-1036 - Настройки

See merge request mobile/Flutter/nut_player!41
2023-10-12 18:58:20 +03:00
Elena Nazarova 92b4be7113 PSDK-1036 - Настройки 2023-10-12 18:58:20 +03:00
Elena Nazarova e1af5417c1 Merge branch 'feature/PSDK-951/file_links' into 'develop'
PSDK-951 - Ссылки

See merge request mobile/Flutter/nut_player!40
2023-10-12 13:46:35 +03:00
Elena Nazarova 9e082faecb PSDK-951 - Ссылки 2023-10-12 13:46:34 +03:00
Jura Shikin 86cc2824c8 Merge branch 'feature/PSDK-1033' into 'develop'
Resolve PSDK-1033 "Feature/"

See merge request mobile/Flutter/nut_player!39
2023-10-12 12:34:18 +03:00
Jura Shikin fe3d7bb507 Resolve PSDK-1033 "Feature/" 2023-10-12 12:34:18 +03:00
Николай Коротков edd46e50eb Merge branch 'feature/PSDK-978/settings' into 'develop'
PSDK-978 - Экран настроек

See merge request mobile/Flutter/nut_player!37
2023-10-11 18:07:49 +03:00
Elena Nazarova 885d75a18e PSDK-978 - Экран настроек 2023-10-11 18:07:48 +03:00
Juraldinio 909f35c1b9 Fix json coding 2023-10-11 14:09:19 +03:00
Juraldinio 61db66797f Fix playing content from Firebase 2023-10-11 11:49:48 +03:00
Juraldinio e23a4db2df PSDK-1023 - remove yellow underline 2023-10-11 10:40:39 +03:00
Juraldinio b76ac97214 PSDK-1016 - sync with iOS version of initialize 2023-10-10 22:52:51 +03:00
Juraldinio bb59249479 PSDK-998 - fix show skin on iOS 2023-10-10 22:52:51 +03:00
Николай Коротков e4f7655c8b Merge branch 'feature/PSDK-1003/provider' into 'develop'
PSDK-1003 - Провайдер

See merge request mobile/Flutter/nut_player!36
2023-10-10 13:10:53 +03:00
Elena Nazarova 624ef8c9a8 PSDK-1003 - Провайдер 2023-10-10 13:10:53 +03:00
Juraldinio a10a917742 PSDK-1013 - Crashlytics in flutter 2023-10-10 11:39:09 +03:00
Juraldinio a9d948a19e PSDK-1012 - implement dispose method. Fix crash on Android. 2023-10-10 10:35:32 +03:00
Juraldinio f035153cb6 PSDK-1006 - fix subscription to stream more then once 2023-10-10 09:37:47 +03:00
Juraldinio a9afdca7ea PSDK-1015 - add supporting custom providers for player 2023-10-09 22:52:51 +03:00
Juraldinio 6fd970aaf4 PSDK-1009 - fix autopublish firebase 2023-10-09 11:44:44 +03:00
Juraldinio d0c557692e Update minimal version to 13 2023-10-06 19:21:33 +03:00
Juraldinio 054b7f0b2b Update NutPlayer framework 2023-10-06 19:21:33 +03:00
Jura Shikin 35c5b2d8e7 Merge branch 'feature/PSDK-1003/fb_provider' into 'develop'
PSDK-1003 - ФБ провайдер

See merge request mobile/Flutter/nut_player!35
2023-10-06 19:20:57 +03:00
Elena Nazarova c3d699e385 PSDK-1003 - ФБ провайдер 2023-10-06 19:20:57 +03:00
Jura Shikin af004e86ab Merge branch 'feature/PSDK-1003/content_inputs' into 'develop'
PSDK-1003 - Примеры ссылок

See merge request mobile/Flutter/nut_player!33
2023-10-06 11:29:39 +03:00
Elena Nazarova 640392fcd0 PSDK-1003 - Примеры ссылок 2023-10-06 11:29:39 +03:00
Jura Shikin 6edcade8a5 Merge branch 'feature/PSDK-973/native_implementation_of_the_android_library_interface_logger' into 'develop'
PSDK-973 Добавил логгер событий нат плеера

See merge request mobile/Flutter/nut_player!34
2023-10-05 11:55:45 +03:00
Николай Коротков 3d0ba6e34c PSDK-973 Добавил логгер событий нат плеера 2023-10-05 11:55:45 +03:00
Juraldinio aea1f455cc Add permissions to android 2023-10-04 13:13:47 +03:00
Jura Shikin 84a0cdfbd4 Merge branch 'feature/PSDK-973/native_implementation_of_the_android_library_interface_methods' into 'develop'
PSDK-973 Добавил нативную реализацию методов

See merge request mobile/Flutter/nut_player!32
2023-10-04 13:07:49 +03:00
Николай Коротков 5d85c0a6db PSDK-973 Добавил нативную реализацию методов 2023-10-04 13:07:49 +03:00
Jura Shikin f00efb27da Merge branch 'feature/PSDK-1005/speed_corrections' into 'develop'
PSDK-1005 - Корректировка смены скорости на 2х

See merge request mobile/Flutter/nut_player!31
2023-10-04 12:18:27 +03:00
Elena Nazarova 21519397a6 PSDK-1005 - Корректировка смены скорости на 2х 2023-10-04 12:18:27 +03:00
Jura Shikin c489533ce4 Merge branch 'feature/PSDK-988/settings_update' into 'develop'
PSDK-988 - Обновление значения настройки

See merge request mobile/Flutter/nut_player!30
2023-10-03 20:49:52 +03:00
Elena Nazarova 6dfbd5cf27 PSDK-988 - Обновление значения настройки 2023-10-03 20:49:52 +03:00
Jura Shikin 3e9a152e40 Merge branch 'feature/PSDK-973/native_implementation_of_the_android_library_interface' into 'develop'
PSDK-973 Добавить слушатель состояний плеера и настроить их отправку по каналу

See merge request mobile/Flutter/nut_player!29
2023-10-03 20:48:50 +03:00
Николай Коротков 63db8a4a10 PSDK-973 Добавить слушатель состояний плеера и настроить их отправку по каналу 2023-10-03 20:48:50 +03:00
Elena Nazarova 4832b88906 Merge branch 'feature/PSDK-1000/link' into 'develop'
PSDK-1000 - Подстановка ссылкт

See merge request mobile/Flutter/nut_player!28
2023-10-03 12:20:22 +03:00
Elena Nazarova 0c725023c0 PSDK-1000 - Подстановка ссылкт 2023-10-03 12:20:22 +03:00
Jura Shikin 7234595a5b Merge branch 'feature/PSDK-989/connect_kotlin_to_dart' into 'develop'
PSDK-989 Connect kotlin to dart

See merge request mobile/Flutter/nut_player!18
2023-10-03 12:10:39 +03:00
Николай Коротков 8432d03bbd PSDK-989 Connect kotlin to dart 2023-10-03 12:10:39 +03:00
Juraldinio b9a56be791 Add flutter logger to player 2023-10-02 20:29:55 +03:00
Juraldinio 7f925801c7 Fix seek problem 2023-10-02 15:12:53 +03:00
Juraldinio cb38ad2fc9 Fix for using controller after deinit 2023-10-02 14:46:40 +03:00
Juraldinio 71a8f84fdc Update for dismiss navigation flow 2023-10-02 14:42:47 +03:00
Juraldinio 73ad617c66 Improve bloc for PlayerView 2023-10-02 14:15:12 +03:00
Juraldinio 571fcb783f NSAllowsArbitraryLoads in application 2023-09-29 13:17:06 +03:00
Jura Shikin c748ba6644 Merge branch 'feature/PSDK-988/small_fix' into 'develop'
PSDK-988 - Небольшая правка сброса

See merge request mobile/Flutter/nut_player!27
2023-09-29 12:31:42 +03:00
Elena Nazarova 6abb28bd4b PSDK-988 - Небольшая правка сброса 2023-09-29 12:31:41 +03:00
Juraldinio 1a4000688a Fix events workout 2023-09-29 12:29:47 +03:00
Jura Shikin f96b300ef4 Merge branch 'feature/PSDK-999/fix_problems' into 'develop'
PSDK-999: Исправил проблемы при обновлении буфера и обращении к текущему времени

See merge request mobile/Flutter/nut_player!26
2023-09-29 11:10:44 +03:00
Андрей Геращенко f9b432cc4c PSDK-999: Исправил проблемы при обновлении буфера и обращении к текущему времени 2023-09-29 11:10:44 +03:00
Николай Коротков b4a9e6c225 Merge branch 'feature/PSDK-988/player_bloc' into 'develop'
PSDK-988 - Прокиданы методы

See merge request mobile/Flutter/nut_player!25
2023-09-28 19:43:00 +03:00
Elena Nazarova c6f06652f4 PSDK-988 - Прокиданы методы 2023-09-28 19:42:59 +03:00
Jura Shikin 46e493dd4e Merge branch 'feature/PSDK-998/player_skin' into 'develop'
PSDK-998: Вернул отображение стандартного интерфейса в проигрыватель

See merge request mobile/Flutter/nut_player!23
2023-09-28 19:36:52 +03:00
Андрей Геращенко 7d416fa3d2 PSDK-998: Вернул отображение стандартного интерфейса в проигрыватель 2023-09-28 19:36:52 +03:00
Jura Shikin f9e2fa7578 Merge branch 'feature/PSDK-999/fix_problems' into 'develop'
PSDK-999: Исправил ошибку состояния проигрывателя, которое отключало кнопки управления

See merge request mobile/Flutter/nut_player!24
2023-09-28 18:17:25 +03:00
Андрей Геращенко 136aa5d593 PSDK-999: Исправил ошибку состояния проигрывателя, которое отключало кнопки управления 2023-09-28 18:17:25 +03:00
Elena Nazarova 2f5d985ac1 Merge branch 'feature/PSDK-996/content_provider' into 'develop'
PSDK-996: Добавил обработку провайдера и удалил лишнее

See merge request mobile/Flutter/nut_player!21
2023-09-28 13:30:41 +03:00
Андрей Геращенко 66f04ec833 PSDK-996: Добавил обработку провайдера и удалил лишнее 2023-09-28 13:30:40 +03:00
Elena Nazarova e69984cbf1 Merge branch 'feature/PSDK-997/player_events' into 'develop'
PSDK-997: Добавлена обработка событий

See merge request mobile/Flutter/nut_player!22
2023-09-28 11:20:13 +03:00
Андрей Геращенко 8101052ab2 PSDK-997: Добавлена обработка событий 2023-09-28 11:20:12 +03:00
Андрей Геращенко 0f4fcd2342 Merge branch 'feature/PSDK-992/refactoring' into 'develop'
PSDK-992 - Правки наследования

See merge request mobile/Flutter/nut_player!19
2023-09-27 14:07:57 +03:00
Elena Nazarova c55db303a0 PSDK-992 - Правки наследования 2023-09-27 14:07:57 +03:00
Elena Nazarova 29b9e8a6b7 Merge branch 'ios_plugin_implement' into 'develop'
Playing video in UIView

See merge request mobile/Flutter/nut_player!20
2023-09-27 11:31:10 +03:00
Jura Shikin 8d2d0b3fb0 Playing video in UIView 2023-09-27 11:31:10 +03:00
Николай Коротков ff441f97ce Merge branch 'feature/PSDK-986/url_bloc' into 'develop'
PSDK-986 - URL bloc

See merge request mobile/Flutter/nut_player!16
2023-09-25 11:23:24 +03:00
Elena Nazarova dee04293ee PSDK-986 - URL bloc 2023-09-25 11:23:24 +03:00
Elena Nazarova 50e2608f94 Merge branch 'feature/PSDK-989/simply_init_android_methods' into 'develop'
PSDK-989 Simply init android methods

See merge request mobile/Flutter/nut_player!15
2023-09-22 16:26:24 +03:00
Николай Коротков 4432a4326c PSDK-989 Simply init android methods 2023-09-22 16:26:23 +03:00
Jura Shikin e12f6886bf Merge branch 'feature/PSDK-983/init_methods' into 'develop'
PSDK-983: Методы инициализации общего интерфейса на платформе иос

See merge request mobile/Flutter/nut_player!12
2023-09-21 20:24:01 +03:00
Андрей Геращенко 43eda35216 PSDK-983: Методы инициализации общего интерфейса на платформе иос 2023-09-21 20:24:00 +03:00
Андрей Геращенко 6d89641fa8 Merge branch 'feature/PSDK-977/json_bloc' into 'develop'
PSDK-977 - Json bloc

See merge request mobile/Flutter/nut_player!13
2023-09-21 17:25:00 +03:00
Elena Nazarova 290fe952dc PSDK-977 - Json bloc 2023-09-21 17:25:00 +03:00
199 changed files with 46180 additions and 13135 deletions
+60 -37
View File
@@ -1,21 +1,18 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
# Build template
.BuildApplication: &BuildApplication
when: on_success
allow_failure: false
Build:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
Build:
<<: *BuildApplication
only:
refs:
- develop
- tags
artifacts:
paths:
- ./Result
@@ -25,11 +22,14 @@ Build:
- ./toolchain/build_platforms.sh nut_player device
ManualBuild:
<<: *BuildApplication
when: manual
only:
refs:
- /^feature/
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
when: manual
allow_failure: false
tags:
- macos
- flutter
stage: build
artifacts:
paths:
- ./Result
@@ -39,8 +39,13 @@ ManualBuild:
- ./toolchain/build_platforms.sh nut_player device
Deploy:
when: on_success
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
tags:
- macos
- flutter
@@ -54,10 +59,6 @@ Deploy:
IPA_FILE: NutPlayerFlutter.ipa
IPA_PATH: ./Result
stage: deploy
only:
refs:
- develop
- tags
script:
- chmod +x ./toolchain/deploy_iOS.sh
- chmod +x ./toolchain/deploy_Android.sh
@@ -65,8 +66,10 @@ Deploy:
- ./toolchain/deploy_Android.sh nut_player
Deploy_Feature:
when: manual
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
when: manual
allow_failure: false
tags:
- macos
- flutter
@@ -80,9 +83,6 @@ Deploy_Feature:
IPA_FILE: NutPlayerFlutter.ipa
IPA_PATH: ./Result
stage: deploy
only:
refs:
- /^feature/
script:
- chmod +x ./toolchain/deploy_iOS.sh
- chmod +x ./toolchain/deploy_Android.sh
@@ -90,14 +90,16 @@ Deploy_Feature:
- ./toolchain/deploy_iOS.sh nut_player ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${IPA_FILE} iOS
notifyMessengerFail:
when: on_failure
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_failure
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_failure
allow_failure: false
needs:
- job: Deploy
artifacts: false
only:
refs:
- develop
- tags
stage: notify
tags:
- macos
@@ -107,14 +109,16 @@ notifyMessengerFail:
- ./toolchain/notification.sh FAIL
notifyMessengerSuccess:
when: on_success
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
needs:
- job: Deploy
artifacts: false
only:
refs:
- develop
- tags
stage: notify
tags:
- macos
@@ -122,3 +126,22 @@ notifyMessengerSuccess:
script:
- chmod +x ./toolchain/notification.sh
- ./toolchain/notification.sh SUCCESS
#ManualUpdateApplications:
# rules:
# - if: $CI_COMMIT_REF_NAME == "develop"
# when: manual
# allow_failure: false
# - if: $CI_COMMIT_TAG != null
# when: manual
# allow_failure: false
# tags:
# - macos
# - flutter
# stage: notify
# script:
# - |
# curl -X "POST" "https://gitlab.nut.team/api/v4/projects/574/trigger/pipeline" \
# -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
# --data-urlencode "token=$K8S_TOKEN_UPDATE_CONFRONTARE_APPLICATION" \
# --data-urlencode "ref=develop"
+11 -14
View File
@@ -3,46 +3,43 @@ variables:
# Linting stage
Lint:
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: always
stage: lint
when: always
tags:
- macos
- flutter
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/linting.sh
- ./toolchain/linting.sh nut_player
# Testing stage
Tests:
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: on_success
allow_failure: false
stage: test
when: on_success
allow_failure: false
tags:
- macos
- flutter
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/testing.sh
- ./toolchain/testing.sh nut_player
# Build Application
mrBuildApplication:
when: on_success
allow_failure: false
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
needs:
- job: Tests
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/build_platforms.sh
- ./toolchain/build_platforms.sh nut_player simulator
+4
View File
@@ -1,5 +1,8 @@
before_script:
- export PATH="$PATH:$FLUTTER_PATH"
- echo "CI_PIPELINE_SOURCE = $CI_PIPELINE_SOURCE"
- echo "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME = $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
- echo "CI_DEFAULT_BRANCH = $CI_DEFAULT_BRANCH"
stages:
- lint
@@ -11,3 +14,4 @@ stages:
include:
- '.before-merge-request.yml'
- '.after-merge-request.yml'
- '.triggered.yml'
+72
View File
@@ -0,0 +1,72 @@
UpdateDependentLibraries:
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" && ($CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "feature/autoupdate")
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
script:
- |
echo "♻️ Update library for nut_player"
echo $LIBRARY_TYPE
echo $LIBRARY_PATH
if [ -z "$LIBRARY_PATH" ]; then
echo "⛔️ Path not passed."
exit 1
fi
echo "💈 Download library"
if [ "$LIBRARY_TYPE" == "ios" ]; then
wget --header "PRIVATE-TOKEN: $K8S_TOKEN_LIBRARY_IOS" "$LIBRARY_PATH" -O artifact.zip
echo "📦 Unzip library"
unzip artifact.zip
echo "📲 Copy library"
cp -R ios/Result/NutPlayer/NutPlayer.xcframework nut_player_ios/ios/Vendors
echo "🗑️ Remove temporary"
rm artifact.zip
rm -rf ios
echo "⚙️ Add to Git new library"
git add -A && git commit -m "Update iOS library [CI/CD]"
git status
project_url=$(echo $CI_PROJECT_URL | sed 's/https:\/\///')
git remote set-url origin https://oauth2:$K8S_TOKEN_UPLOAD_LIBRARY@$project_url
git push origin HEAD:$CI_COMMIT_REF_NAME
fi
if [ "$LIBRARY_TYPE" == "android" ]; then
wget --header "PRIVATE-TOKEN: $K8S_TOKEN_LIBRARY_ANDROID" "$LIBRARY_PATH" -O artifact.zip
echo "📦 Unzip library"
unzip artifact.zip
echo "🤖 Copy library"
cp -R android/artifacts/*.aar nut_player_android/android/libs
echo "🗑️ Remove temporary"
rm artifact.zip
rm -rf android
echo "⚙️ Add to Git new library"
git add -A && git commit -m "Update Android library [CI/CD]"
git status
project_url=$(echo $CI_PROJECT_URL | sed 's/https:\/\///')
git remote set-url origin https://oauth2:$K8S_TOKEN_UPLOAD_LIBRARY@$project_url
git push origin HEAD:$CI_COMMIT_REF_NAME
fi
+31 -11
View File
@@ -5,27 +5,42 @@ plugins {
id 'com.google.gms.google-services'
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
def flutterSdkVersions = new Properties()
def flutterSdkVersionsFile = rootProject.file('flutterSdkVersions.properties')
if (flutterSdkVersionsFile.exists()) {
flutterSdkVersionsFile.withReader('UTF-8') { reader ->
flutterSdkVersions.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionCode = flutterSdkVersions.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterVersionName = flutterSdkVersions.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def flutterCompileSdkVersion = flutterSdkVersions.getProperty('flutter.compileSdkVersion')
if (flutterCompileSdkVersion == null) {
flutterCompileSdkVersion = 1
}
def flutterTargetSdkVersion = flutterSdkVersions.getProperty('flutter.targetSdkVersion')
if (flutterTargetSdkVersion == null) {
flutterTargetSdkVersion = 1
}
def flutterMinSdkVersion = flutterSdkVersions.getProperty('flutter.minSdkVersion')
if (flutterMinSdkVersion == null) {
flutterMinSdkVersion = 1
}
android {
namespace "tech.nut.nutplayer.flutter"
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion flutterCompileSdkVersion.toInteger()
ndkVersion flutter.ndkVersion
compileOptions {
@@ -46,8 +61,8 @@ android {
applicationId "tech.nut.nutplayer.flutter"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
minSdkVersion flutterMinSdkVersion
targetSdkVersion flutterTargetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -57,6 +72,8 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
shrinkResources false
minifyEnabled false
}
}
}
@@ -65,4 +82,7 @@ flutter {
source '../..'
}
dependencies {}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
implementation(platform("com.google.firebase:firebase-storage-ktx"))
}
@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,8 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="nut_player_example"
android:networkSecurityConfig="@xml/network_security_config"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -30,4 +32,5 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,11 @@
<?xml version ="1.0" encoding ="utf-8"?><!-- Learn More about how to use App Actions: https://developer.android.com/guide/actions/index.html -->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
+6 -2
View File
@@ -6,9 +6,9 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:8.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:+'
classpath 'com.google.gms:google-services:4.3.15'
}
}
@@ -16,6 +16,10 @@ allprojects {
repositories {
google()
mavenCentral()
maven {
// [required] aar plugin// [required] aar plugin
url "${project(':nut_player_android').projectDir}/build"
}
}
}
@@ -0,0 +1,5 @@
flutter.versionName=1.0
flutter.versionCode=1
flutter.compileSdkVersion=33
flutter.minSdkVersion=21
flutter.targetSdkVersion=33
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableDexingArtifactTransform=false
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+142 -7
View File
@@ -1,28 +1,163 @@
PODS:
- Firebase/CoreOnly (10.15.0):
- FirebaseCore (= 10.15.0)
- Firebase/Crashlytics (10.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.15.0)
- Firebase/Storage (10.15.0):
- Firebase/CoreOnly
- FirebaseStorage (~> 10.15.0)
- firebase_core (2.17.0):
- Firebase/CoreOnly (= 10.15.0)
- Flutter
- firebase_crashlytics (3.3.7):
- Firebase/Crashlytics (= 10.15.0)
- firebase_core
- Flutter
- firebase_storage (11.2.8):
- Firebase/Storage (= 10.15.0)
- firebase_core
- Flutter
- FirebaseAppCheckInterop (10.16.0)
- FirebaseAuthInterop (10.16.0)
- FirebaseCore (10.15.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Logger (~> 7.8)
- FirebaseCoreExtension (10.16.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.16.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseCrashlytics (10.15.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (~> 2.1)
- FirebaseInstallations (10.16.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseSessions (10.16.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.10)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesSwift (~> 2.1)
- FirebaseStorage (10.15.0):
- FirebaseAppCheckInterop (~> 10.0)
- FirebaseAuthInterop (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseCoreExtension (~> 10.0)
- GTMSessionFetcher/Core (< 4.0, >= 2.1)
- Flutter (1.0.0)
- GoogleDataTransport (9.2.5):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Environment (7.11.5):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.11.5):
- GoogleUtilities/Environment
- "GoogleUtilities/NSData+zlib (7.11.5)"
- GoogleUtilities/UserDefaults (7.11.5):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.1.1)
- integration_test (0.0.1):
- Flutter
- nut_player (0.0.1):
- nanopb (2.30909.0):
- nanopb/decode (= 2.30909.0)
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- nut_player_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- PromisesObjC (2.3.1)
- PromisesSwift (2.3.1):
- PromisesObjC (= 2.3.1)
- screen_brightness_ios (0.1.0):
- Flutter
DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_storage (from `.symlinks/plugins/firebase_storage/ios`)
- Flutter (from `Flutter`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- nut_player (from `.symlinks/plugins/nut_player/ios`)
- nut_player_ios (from `.symlinks/plugins/nut_player_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseSessions
- FirebaseStorage
- GoogleDataTransport
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- PromisesObjC
- PromisesSwift
EXTERNAL SOURCES:
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_storage:
:path: ".symlinks/plugins/firebase_storage/ios"
Flutter:
:path: Flutter
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
nut_player:
:path: ".symlinks/plugins/nut_player/ios"
nut_player_ios:
:path: ".symlinks/plugins/nut_player_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
SPEC CHECKSUMS:
Firebase: 66043bd4579e5b73811f96829c694c7af8d67435
firebase_core: 28e84c2a4fcf6a50ef83f47b145ded8c1fa331e4
firebase_crashlytics: 36b8a72e23437dbb69bd97102661ce31b6721be5
firebase_storage: 5aa5cd1cfc03814e3417b6718d805e5d1804f990
FirebaseAppCheckInterop: 82358cff9f33452dd44259e88eea5e562500b1cb
FirebaseAuthInterop: b79fab8ce80e685145eee5f973d9ad5210e19d44
FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e
FirebaseCoreExtension: 2dbc745b337eb99d2026a7a309ae037bd873f45e
FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a
FirebaseCrashlytics: a83f26fb922a3fe181eb738fb4dcf0c92bba6455
FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee
FirebaseSessions: 96e7781e545929cde06dd91088ddbb0841391b43
FirebaseStorage: 1d4be239ea32fb3c0f3680a6f2b706d6cabe37f2
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72
integration_test: 13825b8a9334a850581300559b8839134b124670
nut_player: fac5f2edd6e1927a28413a98d968ff8f67078da2
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
nut_player_ios: c6964e0278d1a01a40929de47bbc7b8bf5bbc8d8
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
COCOAPODS: 1.12.1
COCOAPODS: 1.13.0
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
02000B77C8E488D819458EB8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
@@ -15,7 +14,9 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */; };
9D6C432C130B5C4CCD4F030E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */; };
A4D9603085CECA05CC013C25 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FD33782B308F981743B1D9A /* Pods_Runner.framework */; };
B173FFAC2ACEB6A8009C6CB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -42,18 +43,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0FD33782B308F981743B1D9A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5BBC6444966502149BA91788 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8CEF516E13842BF4F217306E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -61,10 +62,11 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A8D3A5B8216DAA0326943AC2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F2D2F836E458DFB68D190B87 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
FA5F9924AD21CAB1263D7189 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
BF64510E1DC112A547CBFBC5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +74,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
02000B77C8E488D819458EB8 /* Pods_Runner.framework in Frameworks */,
A4D9603085CECA05CC013C25 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -80,13 +82,22 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */,
9D6C432C130B5C4CCD4F030E /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
182DEA855A45E43DC6B6F1B3 /* Frameworks */ = {
isa = PBXGroup;
children = (
0FD33782B308F981743B1D9A /* Pods_Runner.framework */,
C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -98,25 +109,16 @@
40FDE73F8AC767B1B188CE85 /* Pods */ = {
isa = PBXGroup;
children = (
FA5F9924AD21CAB1263D7189 /* Pods-Runner.debug.xcconfig */,
A8D3A5B8216DAA0326943AC2 /* Pods-Runner.release.xcconfig */,
F2D2F836E458DFB68D190B87 /* Pods-Runner.profile.xcconfig */,
6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */,
41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */,
FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */,
BF64510E1DC112A547CBFBC5 /* Pods-Runner.debug.xcconfig */,
5BBC6444966502149BA91788 /* Pods-Runner.release.xcconfig */,
8CEF516E13842BF4F217306E /* Pods-Runner.profile.xcconfig */,
6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */,
AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */,
ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
54C6C191361639295A13AFCC /* Frameworks */ = {
isa = PBXGroup;
children = (
6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */,
91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -136,7 +138,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
40FDE73F8AC767B1B188CE85 /* Pods */,
54C6C191361639295A13AFCC /* Frameworks */,
182DEA855A45E43DC6B6F1B3 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -152,6 +154,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -171,7 +174,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
4EFE922145174E30D57418A9 /* [CP] Check Pods Manifest.lock */,
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
CE669F37B5D4B4901DDD5C29 /* Frameworks */,
@@ -190,14 +193,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */,
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */,
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */,
5C8120DFBB6562B7085522ED /* [firebase_crashlytics] Crashlytics Upload Symbols */,
);
buildRules = (
);
@@ -261,6 +265,7 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
B173FFAC2ACEB6A8009C6CB7 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -269,23 +274,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4EFE922145174E30D57418A9 /* [CP] Check Pods Manifest.lock */ = {
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -307,22 +296,44 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5C8120DFBB6562B7085522ED /* [firebase_crashlytics] Crashlytics Upload Symbols */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
inputPaths = (
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\"",
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/\"",
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"",
"\"$(BUILT_PRODUCTS_DIR)/$(EXECUTABLE_PATH)\"",
"\"$(PROJECT_DIR)/firebase_app_id_file.json\"",
);
name = "[firebase_crashlytics] Crashlytics Upload Symbols";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --flutter-project \"$PROJECT_DIR/firebase_app_id_file.json\" ";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@@ -339,7 +350,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */ = {
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -452,7 +480,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -471,6 +499,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -490,7 +519,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -508,7 +537,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -524,7 +553,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -585,7 +614,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -634,7 +663,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -655,6 +684,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -687,6 +717,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyCZseoWZwl06QT-vrPWPKc46FRkFvegLDo</string>
<key>GCM_SENDER_ID</key>
<string>803206890572</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>tech.nut.nutplayer.flutter</string>
<key>PROJECT_ID</key>
<string>nutplayer-flutter</string>
<key>STORAGE_BUCKET</key>
<string>nutplayer-flutter.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:803206890572:ios:32e52f7f99b39c1602e0e9</string>
</dict>
</plist>
+16 -2
View File
@@ -4,6 +4,8 @@
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -24,18 +26,30 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen.storyboard</string>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
@@ -0,0 +1,7 @@
{
"file_generated_by": "FlutterFire CLI",
"purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
"GOOGLE_APP_ID": "1:803206890572:ios:32e52f7f99b39c1602e0e9",
"FIREBASE_PROJECT_ID": "nutplayer-flutter",
"GCM_SENDER_ID": "803206890572"
}
@@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyB7HEhQGvbebWG-B75L9DcSlq_n0EG_zTc',
appId: '1:803206890572:android:ad09971112d775d202e0e9',
messagingSenderId: '803206890572',
projectId: 'nutplayer-flutter',
storageBucket: 'nutplayer-flutter.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyCZseoWZwl06QT-vrPWPKc46FRkFvegLDo',
appId: '1:803206890572:ios:32e52f7f99b39c1602e0e9',
messagingSenderId: '803206890572',
projectId: 'nutplayer-flutter',
storageBucket: 'nutplayer-flutter.appspot.com',
iosBundleId: 'tech.nut.nutplayer.flutter',
);
}
+36 -7
View File
@@ -1,17 +1,46 @@
import 'package:flutter/material.dart';
import 'src/features/main_screen/presentation/home.dart';
import 'dart:ui';
void main() {
runApp(const MyApp());
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'src/features/main_screen/presentation/home.dart';
import 'package:screen_brightness/screen_brightness.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final double systemBrightness = await ScreenBrightness().system;
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(MyApp(brightness: systemBrightness));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
final double brightness;
const MyApp({required this.brightness, super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Home(),
return RepositoryProvider(
create: (context) => SettingsRepository.createDefaults(brightness),
child: const CupertinoApp( //Для теста убрать const
theme: CupertinoThemeData(
brightness: Brightness.light,
primaryColor: CupertinoColors.link
),
home: Home()//Bместо Home() подставить PlayerView()
)
);
}
}
@@ -1,17 +1,6 @@
import '../models/option_data.dart';
extension OptionsListExtension on List<OptionData> {
void unselectAll() {
forEach((element) {
element.isSelected = false;
});
}
bool hasSelection() {
var selectedElements = where((element) => element.isSelected);
return selectedElements.isNotEmpty;
}
OptionData? selected() {
return where((element) => element.isSelected).first;
}
@@ -1,17 +1,47 @@
import 'package:flutter/cupertino.dart';
class OptionData {
late Key key;
late String title;
late bool isSelected;
final Key key;
final String title;
final String? value;
final bool isLive;
final bool isSelected;
OptionData({required this.key, required this.title, this.isSelected = false});
const OptionData({required this.key, required this.title, this.value, this.isLive = false, this.isSelected = false});
const OptionData.withValueEqualTitle(Key key, String title): this(key: key, title: title, value: title);
}
class BoolOptionData {
final Key key;
final String title;
final bool isSelected;
final Function(bool)? onChange;
BoolOptionData({required this.key, required this.title, required this.isSelected, this.onChange});
}
class OptionDataContainer {
final Key? key;
final String title;
final List<OptionData> options;
final int? selectedIndex;
final Function(OptionData)? onSelectedOption;
OptionDataContainer({
this.key,
required this.title,
required this.options,
this.selectedIndex,
this.onSelectedOption
});
}
class NumericOptionData {
late Key key;
late String title;
late int value;
final Key key;
final String title;
final int value;
final Function(int)? onChange;
NumericOptionData({required this.key, required this.title, required this.value});
const NumericOptionData({required this.key, required this.title, required this.value, this.onChange});
}
@@ -0,0 +1,113 @@
enum RepositoryFullscreenSettings { landscape, flexible, off }
enum RepositorySkinColor { white }
enum RepositoryVideoQuality { auto, p2160, p1440, p1080, p720, p480, p360, p240, p144 }
enum RepositoryVideoQualityNaming { common, rus, eng, resolution }
enum RepositoryLogType { off, info, debug }
extension RepositoryLogTitle on RepositoryLogType {
String get key {
switch (this) {
case RepositoryLogType.info:
return "info";
case RepositoryLogType.debug:
return "debug";
case RepositoryLogType.off:
return "off";
}
}
}
extension RepositoryVideoQualityIdentifier on RepositoryVideoQuality {
String get identifier {
switch (this) {
case RepositoryVideoQuality.auto:
return "auto";
case RepositoryVideoQuality.p144:
return "p144";
case RepositoryVideoQuality.p240:
return "p240";
case RepositoryVideoQuality.p360:
return "p360";
case RepositoryVideoQuality.p480:
return "p480";
case RepositoryVideoQuality.p720:
return "p720";
case RepositoryVideoQuality.p1080:
return "p1080";
case RepositoryVideoQuality.p1440:
return "p1440";
case RepositoryVideoQuality.p2160:
return "p2160";
}
}
}
class SettingsRepository {
late bool isSkinByDefault;
late RepositoryFullscreenSettings fullscreenSettings;
late bool isPipAvailable;
late bool isSettingsAvailable;
late RepositorySkinColor skinColor;
late bool isAutostart;
late double volume;
late double brightness;
late double speed;
late RepositoryVideoQualityNaming qualityNaming;
late RepositoryVideoQuality quality;
late bool isSubtitlesAvailable;
late int start;
late bool isLoop;
late int playlist;
late int track;
late int chunk;
late RepositoryLogType log;
SettingsRepository({
required this.isSkinByDefault,
required this.fullscreenSettings,
required this.isPipAvailable,
required this.isSettingsAvailable,
required this.skinColor,
required this.isAutostart,
required this.brightness,
required this.volume,
required this.speed,
required this.qualityNaming,
required this.quality,
required this.isSubtitlesAvailable,
required this.start,
required this.isLoop,
required this.playlist,
required this.track,
required this.chunk,
required this.log
});
factory SettingsRepository.createDefaults(double brightness) {
return SettingsRepository(
isSkinByDefault: true,
fullscreenSettings: RepositoryFullscreenSettings.landscape,
isPipAvailable: true,
isSettingsAvailable: true,
skinColor: RepositorySkinColor.white,
isAutostart: false,
brightness: brightness,
volume: 0.5,
speed: 1,
qualityNaming: RepositoryVideoQualityNaming.common,
quality: RepositoryVideoQuality.auto,
isSubtitlesAvailable: true,
start: 0,
isLoop: false,
playlist: 5000,
track: 3000,
chunk: 3000,
log: RepositoryLogType.info
);
}
}
@@ -1,19 +1,19 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class CheckmarkListOption extends StatelessWidget {
final OptionData data;
final String title;
final bool isSelected;
final Function? onTap;
const CheckmarkListOption(this.data, this.onTap, {super.key});
const CheckmarkListOption(this.title, this.isSelected, this.onTap, {super.key});
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: data.key,
key: key,
onTap: () { if (onTap != null) { onTap!(); } },
trailing: data.isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
title: Text(data.title, style: const TextStyle(fontSize: 17))
trailing: isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
);
}
}
@@ -1,31 +1,106 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
class InputView extends StatelessWidget {
class InputView extends StatefulWidget {
final String title;
final int value;
final Function(int)? newSelection;
final NumericOptionData data;
const InputView(this.title, this.value, this.newSelection, {super.key});
@override
State<InputView> createState() => _InputViewState();
}
class _InputViewState extends State<InputView> {
final TextEditingController _controller = TextEditingController();
InputView(this.data, {super.key});
final FocusNode _nodeText = FocusNode();
late String _textBefore;
@override
Widget build(BuildContext context) {
_controller.text = '${data.value}';
_controller.text = '${widget.value}';
_textBefore = '${widget.value}';
return CupertinoListTile(
key: data.key,
key: widget.key,
trailing: SizedBox(
width: 155,
child: CupertinoTextField(
minLines: 1,
maxLines: 1,
maxLength: 10,
autocorrect: false,
keyboardType: TextInputType.number,
controller: _controller,
decoration: null,
),
width: 155,
height: 30,
child: KeyboardActions(
tapOutsideBehavior: TapOutsideBehavior.translucentDismiss,
config: _buildConfig(context),
child: Container(
padding: const EdgeInsets.only(left: 12),
child: CupertinoTextField(
minLines: 1,
maxLines: 1,
maxLength: 10,
autocorrect: false,
focusNode: _nodeText,
decoration: null,
keyboardType: TextInputType.number,
controller: _controller,
onSubmitted: _onSubmit,
onTapOutside: (_) {
_controller.text = _textBefore;
},
),
),
)
),
title: Text(data.title, style: const TextStyle(fontSize: 17))
title: Text(widget.title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17)));
}
void _onSubmit(String newText) {
_textBefore = newText;
var newTime = int.tryParse(newText);
if (newTime != null) {
widget.newSelection?.call(newTime);
}
}
KeyboardActionsConfig _buildConfig(BuildContext context) {
return KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: CupertinoColors.white,
keyboardSeparatorColor: CupertinoColors.systemGrey4,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _nodeText,
toolbarButtons: [
(node) {
return TextFieldTapRegion(
child: Container(
width: MediaQuery.of(context).size.width - 14,
margin: const EdgeInsets.symmetric(horizontal: 7),
child: Row(
children: [
CupertinoButton(
padding: const EdgeInsets.all(5),
onPressed: () {
_controller.text = _textBefore;
node.unfocus();
},
child: const Text("Отмена", style: TextStyle(fontWeight: FontWeight.w500))
),
const Spacer(),
CupertinoButton(
padding: const EdgeInsets.all(5),
onPressed: () {
_onSubmit(_controller.text);
node.unfocus();
},
child: const Text("Готово", style: TextStyle(fontWeight: FontWeight.w500))
)
],
),
),
);
}
],
),
],
);
}
}
}
@@ -1,54 +1,70 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/extensions/options_list_extension.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class OptionsView extends StatelessWidget {
final String title;
final List<OptionData> options;
final int? selectedIndex;
final Function(int)? newSelection;
const OptionsView(this.title, this.options, {super.key});
const OptionsView(this.title, this.options, this.selectedIndex, this.newSelection, {super.key});
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: key,
onTap: () { _showAlertDialog(context);},
trailing: Row(
children: [
if (options.selected() != null)
Text(options.selected()!.title,
style: const TextStyle(
fontSize: 17,
color: CupertinoColors.systemGrey,
fontWeight: FontWeight.w400)),
const Icon(CupertinoIcons.chevron_forward,
color: CupertinoColors.systemGrey2)
],
),
title: Text(title, style: const TextStyle(fontSize: 17)));
key: key,
onTap: () { _showAlertDialog(context);},
title: Row(
children: [
Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17)),
const SizedBox(width: 30),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Text(options[selectedIndex ?? 0].title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
color: CupertinoColors.systemGrey,
fontWeight: FontWeight.w400,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
),
)
),
const Icon(CupertinoIcons.chevron_forward, color: CupertinoColors.systemGrey2)
]
)
);
}
void _showAlertDialog(BuildContext context) {
showCupertinoModalPopup<void>(
context: context,
barrierColor: CupertinoColors.black.withOpacity(0.5),
builder: (BuildContext context) => CupertinoActionSheet(
title: Text(title),
actions: <CupertinoActionSheetAction>[...options.map((value) {
return CupertinoActionSheetAction(
child: Text(value.title),
onPressed: () {
Navigator.pop(context);
}
);
}).toList() + [
CupertinoActionSheetAction(
title: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: CupertinoColors.darkBackgroundGray)),
actions: <Widget>[...options.indexed.map((valueIndex) {
return Container(
color: CupertinoColors.white.withOpacity(0.8),
child: CupertinoActionSheetAction(
onPressed: () {
newSelection?.call(valueIndex.$1);
Navigator.pop(context);
},
child: Text(valueIndex.$2.title)
),
);
}).toList()],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('Отмена'),
)]
],
),
),
);
}
@@ -0,0 +1,94 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_content.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_error.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_subtitles.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_statistics.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/parsing/response.dart';
class FirebaseProvider extends Provider {
final String fileName;
static const root = "PlayableContent/";
FirebaseProvider(this.fileName);
static Future<List<String>> getConfigNames() async {
var storage = FirebaseStorage.instance.ref();
var values = await storage.child(root).listAll();
return values.items.map((e) => e.name).toList();
}
@override
Future<PlayerContent> retrieveContent() async {
var storage = FirebaseStorage.instance.ref();
final file = storage.child("$root$fileName");
try {
var results = await file.getData(5 * 1024 * 1024);
return _handleLoadedData(results);
} catch (error) {
var fbError = FirebaseGetDataError(error);
throw Future.error(fbError);
}
}
FirebaseContent _handleLoadedData(Uint8List? data) {
if (data == null) { throw FirebaseEmptyDataError(); }
var encoded = base64Encode(data);
var decoded = utf8.fuse(base64).decode(encoded);
var json = jsonDecode(decoded);
var response = Response.fromJson(json);
if (response.playbacks.isEmpty) { throw FirebaseNoPlaybacksError(); }
var uri = Uri.tryParse(response.playbacks.first.streamUrl);
if (uri != null) {
final statistics = response.statistics.map((stat) =>
FirebaseStatistics(
stat.name,
stat.urlTemplate,
stat.start,
stat.delay,
stat.count,
stat.method == "get" ? HTTPMethod.get : HTTPMethod.post,
stat.body
)
);
final subtitles = response.subtitles.map((sub) =>
FirebaseSubtitles(
sub.title,
sub.type == "srt" ? SubtitleType.srt : SubtitleType.unknown,
sub.url,
sub.language
)
);
final ContentType? contentType;
final urlPath = uri.toString();
if (urlPath.endsWith('.mp4')) {
contentType = Mp4ContentType(urlPath: urlPath);
} else if (urlPath.endsWith('.m3u8')) {
final isLive = response.playbacks.first.isLive;
contentType = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
contentType = null;
}
if (contentType != null) {
return FirebaseContent(
contentType,
statistics.toList(),
subtitles.toList()
);
} else {
throw FirebaseUnknownFormatError();
}
} else {
throw FirebaseIncorrectUrlError();
}
}
}
@@ -0,0 +1,14 @@
import 'package:nut_player/nut_player.dart';
class FirebaseContent extends PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
FirebaseContent(this.content, this.statistics, this.subtitles);
}
@@ -0,0 +1,9 @@
class FirebaseGetDataError extends Error {
final Object innerError;
FirebaseGetDataError(this.innerError);
}
class FirebaseEmptyDataError extends Error {}
class FirebaseNoPlaybacksError extends Error {}
class FirebaseIncorrectUrlError extends Error {}
class FirebaseUnknownFormatError extends Error {}
@@ -0,0 +1,26 @@
import 'package:nut_player/nut_player.dart';
class FirebaseStatistics extends PlayerStatisticRecord {
@override
String name;
@override
String urlTemplate;
@override
double start;
@override
double delay;
@override
int count;
@override
HTTPMethod method;
@override
String? body;
FirebaseStatistics(this.name, this.urlTemplate, this.start, this.delay, this.count, this.method, this.body);
}
@@ -0,0 +1,17 @@
import 'package:nut_player/nut_player.dart';
class FirebaseSubtitles extends PlayerSubtitleRecord {
@override
String title;
@override
SubtitleType type;
@override
String url;
@override
String language;
FirebaseSubtitles(this.title, this.type, this.url, this.language);
}
@@ -0,0 +1,19 @@
class PlaybackRecord {
final String streamType;
final String streamUrl;
final bool isLive;
final bool isVideo;
final bool isAudio;
PlaybackRecord({required this.streamType, required this.streamUrl, required this.isLive, required this.isVideo, required this.isAudio});
factory PlaybackRecord.fromJson(dynamic data) {
return PlaybackRecord(
streamType: data['stream_type'],
streamUrl: data['stream_url'],
isLive: data['is_live'],
isVideo: data['is_video'],
isAudio: data['is_audio']
);
}
}
@@ -0,0 +1,23 @@
import 'statistic_record.dart';
import 'subtitle_record.dart';
import 'playback_record.dart';
class Response {
final List<PlaybackRecord> playbacks;
final List<StatisticRecord> statistics;
final List<PlayerSubtitleRecord> subtitles;
Response({required this.playbacks, required this.statistics, required this.subtitles});
factory Response.fromJson(dynamic data) {
var playbacks = data['playback'].map((item) => PlaybackRecord.fromJson(item)).toList();
var statistics = data['stat'].map((item) => StatisticRecord.fromJson(item)).toList();
var subtitles = data['subtitle'].map((item) => PlayerSubtitleRecord.fromJson(item)).toList();
return Response(
playbacks: List<PlaybackRecord>.from(playbacks),
statistics: List<StatisticRecord>.from(statistics),
subtitles: List<PlayerSubtitleRecord>.from(subtitles)
);
}
}
@@ -0,0 +1,23 @@
class StatisticRecord {
final String name;
final String urlTemplate;
final double start;
final double delay;
final int count;
final String method;
final String? body;
StatisticRecord({required this.name, required this.urlTemplate, required this.start, required this.delay, required this.count, required this.method, this.body});
factory StatisticRecord.fromJson(dynamic data) {
return StatisticRecord(
name: data['name'],
urlTemplate: data['url_template'],
start: data['start'] == null ? 0.0 : data['start'].toDouble(),
delay: data['delay'] == null ? 0.0 : data['delay'].toDouble(),
count: data['count'],
method: data['method'],
body: data['body'],
);
}
}
@@ -0,0 +1,17 @@
class PlayerSubtitleRecord {
final String title;
final String type;
final String url;
final String language;
PlayerSubtitleRecord({required this.title, required this.type, required this.url, required this.language});
factory PlayerSubtitleRecord.fromJson(dynamic data) {
return PlayerSubtitleRecord(
title: data['title'],
type: data['type'],
url: data['url'],
language: data['language']
);
}
}
@@ -0,0 +1,74 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/firebase_provider.dart';
part 'json_event.dart';
part 'json_state.dart';
class JsonBloc extends Bloc<JsonEvent, JsonState> {
JsonBloc() : super(JsonState.create()) {
on<ChooseWayToGetConfigEvent>(_onChooseWayToGetConfigEvent);
on<ChooseWayToGetLinkEvent>(_onChooseWayToGetLinkEvent);
on<ChooseFirebaseConfigEvent>(_onChooseFirebaseConfigEvent);
on<ChooseExampleLinkEvent>(_onChooseExampleLinkEvent);
on<TypeOwnLinkEvent>(_onTypeOwnLinkEvent);
on<JsonResetEvent>(_onResetEvent);
}
_onResetEvent(JsonResetEvent event, Emitter<JsonState> emit) {
emit(JsonState.create());
}
_onChooseWayToGetConfigEvent(ChooseWayToGetConfigEvent event, Emitter<JsonState> emit) async {
JsonState? parentState = state;
while (parentState?.parent != null) { parentState = parentState?.parent; }
if (event.selectedIndex == 0) {
emit(ConfigLinkChosenOptionState.create(parentState));
} else {
emit(FirebaseChosenOptionState.create(parentState, const [], null));
var fileNames = await FirebaseProvider.getConfigNames();
emit(FirebaseChosenOptionState.create(parentState, _createFBOptions(fileNames), null));
}
}
List<OptionData> _createFBOptions(List<String> names) {
return names.indexed.map((e) =>
OptionData.withValueEqualTitle(
Key("ListElementConfig${e.$1}ID"),
e.$2)
).toList();
}
_onChooseWayToGetLinkEvent(ChooseWayToGetLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseLinkExampleState.create(parent: parentState, selectedIndex: null)
: UseJsonOwnLinkState.create(parent: parentState, urlPath: null, isLive: null));
}
_onChooseFirebaseConfigEvent(ChooseFirebaseConfigEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(FirebaseChosenOptionState.create(parentState, state.options, event.selectedIndex));
}
_onChooseExampleLinkEvent(ChooseExampleLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseLinkExampleState.create(parent: parentState, selectedIndex: event.selectedIndex));
}
_onTypeOwnLinkEvent(TypeOwnLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseJsonOwnLinkState.create(parent: parentState, urlPath: event.urlPath, isLive: event.isLive));
}
}
@@ -0,0 +1,32 @@
part of 'json_bloc.dart';
@immutable
sealed class JsonEvent {}
class JsonResetEvent extends JsonEvent {}
class ChooseWayToGetConfigEvent extends JsonEvent {
final int selectedIndex;
ChooseWayToGetConfigEvent(this.selectedIndex);
}
class ChooseWayToGetLinkEvent extends JsonEvent {
final int selectedIndex;
ChooseWayToGetLinkEvent(this.selectedIndex);
}
class ChooseFirebaseConfigEvent extends JsonEvent {
final int selectedIndex;
ChooseFirebaseConfigEvent(this.selectedIndex);
}
class ChooseExampleLinkEvent extends JsonEvent {
final int selectedIndex;
ChooseExampleLinkEvent(this.selectedIndex);
}
class TypeOwnLinkEvent extends JsonEvent {
final String urlPath;
final bool? isLive;
TypeOwnLinkEvent(this.urlPath, this.isLive);
}
@@ -0,0 +1,234 @@
part of 'json_bloc.dart';
typedef JsonStateIndexedEventCreator = JsonEvent Function(int index);
typedef JsonStateStringEventCreator = JsonEvent Function(String value);
enum UIType { list, input }
/// Состояние по умолчанию
@immutable
class JsonState {
final String title;
final Key key;
final List<OptionData> options;
final String? urlPath;
final bool? isLive;
final int? initialIndex;
final int? currentIndex;
final JsonState? parent;
final bool isFinal;
final UIType uiType;
final JsonStateIndexedEventCreator? createIndexedEvent;
final JsonStateStringEventCreator? createStringEvent;
const JsonState({
required this.title,
required this.key,
this.options = const [],
this.urlPath,
this.isLive,
this.initialIndex,
this.currentIndex,
this.parent,
this.isFinal = false,
required this.uiType,
this.createIndexedEvent,
this.createStringEvent
});
factory JsonState.create() {
return JsonState(
title: '1. Выберите способ получения конфига',
key: const Key('ChooseWayToGetConfigListID'),
options: const [
OptionData(key: Key('ListElementConfigLinkID'), title: 'Ссылка на конфиг'),
OptionData(key: Key('ListElementFirebaseID'), title: 'Firebase')
],
uiType: UIType.list,
createIndexedEvent: (index) => ChooseWayToGetConfigEvent(index),
);
}
}
/// Состояние, когда выбран пункт "Ссылка на конфиг"
class ConfigLinkChosenOptionState extends JsonState {
const ConfigLinkChosenOptionState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
JsonState? parent,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ConfigLinkChosenOptionState.create(JsonState? parent) {
return ConfigLinkChosenOptionState(
'2. Выберите способ получения ссылки',
const Key('ChooseWayToGetUrlListID'),
const [
OptionData(key: Key('ListElementUseUrlExampleID'), title: 'Использовать пример ссылки'),
OptionData(key: Key('ListElementUseOwnUrlID'), title: 'Указать свою')
],
0,
parent,
UIType.list,
(index) => ChooseWayToGetLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Firebase"
class FirebaseChosenOptionState extends JsonState {
const FirebaseChosenOptionState(
String title,
Key key,
List<OptionData> options,
String? urlPath,
bool? isLive,
int? initialIndex,
int? currentIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory FirebaseChosenOptionState.create(JsonState? parent, List<OptionData> options, int? selectedIndex) {
final String? currentUrlPath;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
} else {
currentUrlPath = null;
}
return FirebaseChosenOptionState(
'2. Выберите файл конфига',
const Key('ChooseFirebasebConfigListID'),
options,
currentUrlPath,
false,
1,
selectedIndex,
parent,
selectedIndex != null,
UIType.list,
(index) => ChooseFirebaseConfigEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Использовать пример ссылки"
class UseLinkExampleState extends JsonState {
const UseLinkExampleState(
String title,
Key key,
String? urlPath,
List<OptionData> options,
int? initialIndex,
int? currentIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
urlPath: urlPath,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory UseLinkExampleState.create({JsonState? parent, int? selectedIndex}) {
var options = const [
OptionData(key: Key('ListElementUrl1ID'), title: 'Статистика (GET)', value: 'http://chest-101.gc.nut.team:8000/play/opt/5edd1215b2e34a3eb70654a117ea2935'),
OptionData(key: Key('ListElementUrl2ID'), title: 'Статистика (POST)', value: 'http://chest-101.gc.nut.team:8000/play/opt/5edd1215b2e34a3eb70654a117ea2937'),
OptionData(key: Key('ListElementUrl3ID'), title: 'Субтитры (SRT)', value: 'http://chest-101.gc.nut.team:8000/play/opt/e1dc888381e04b4290924dd9ec7b33ed'),
];
final String? currentUrlPath;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
} else {
currentUrlPath = null;
}
return UseLinkExampleState(
'3. Выберите пример ссылки',
const Key('ChooseUrlExampleListID'),
currentUrlPath,
options,
0,
selectedIndex,
parent,
selectedIndex != null,
UIType.list,
(index) => ChooseExampleLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Указать свою"
class UseJsonOwnLinkState extends JsonState {
const UseJsonOwnLinkState(
String title,
Key key,
String? urlPath,
bool? isLive,
int? initialIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createStringEvent: createStringEvent
);
factory UseJsonOwnLinkState.create({JsonState? parent, String? urlPath, bool? isLive}) {
var finalUrlPath = urlPath ?? '';
return UseJsonOwnLinkState(
'3. Укажите ссылку',
const Key('TypeOwnUrlViewID'),
finalUrlPath,
isLive,
1,
parent,
finalUrlPath.isNotEmpty,
UIType.input,
(value) => TypeOwnLinkEvent(value, isLive)
);
}
}
@@ -0,0 +1,59 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/firebase_provider.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
part 'main_event.dart';
part 'main_state.dart';
enum InputType { url, json }
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(const ChosenJsonState()) {
on<ChooseJsonEvent>(_onChooseJson);
on<ChooseUrlEvent>(_onChooseUrl);
on<OpenSettingsEvent>(_onOpenSettings);
on<OpenPlayerEvent>(_onOpenPlayer);
}
_onChooseJson(ChooseJsonEvent event, Emitter<MainState> emit) {
emit(const ChosenJsonState());
}
_onChooseUrl(ChooseUrlEvent event, Emitter<MainState> emit) {
emit(const ChosenUrlState());
}
_onOpenSettings(OpenSettingsEvent event, Emitter<MainState> emit) {
emit(SettingState(state.inputType));
}
_onOpenPlayer(OpenPlayerEvent event, Emitter<MainState> emit) {
Provider? provider;
final jsonUrlPath = event.jsonState.urlPath;
final urlUrlPath = event.urlState.urlPath;
if (event.jsonState.isFinal && jsonUrlPath != null) {
if (event.jsonState is FirebaseChosenOptionState) {
provider = FirebaseProvider(jsonUrlPath);
} else if (event.jsonState is UseJsonOwnLinkState || event.jsonState is UseLinkExampleState) {
provider = JsonProvider(jsonUrlPath);
}
// TODO: Поддежка Json-конфигов
} else if (event.urlState.isFinal && urlUrlPath != null) {
provider = CommonProvider.url(
urlUrlPath,
isLive: event.urlState.isLive ?? false,
isLoop: event.isLoop
);
}
if (provider != null) {
emit(PlayerState(state.inputType, provider));
}
}
}
@@ -0,0 +1,15 @@
part of 'main_bloc.dart';
@immutable
sealed class MainEvent {}
class ChooseJsonEvent extends MainEvent {}
class ChooseUrlEvent extends MainEvent {}
class OpenSettingsEvent extends MainEvent {}
class OpenPlayerEvent extends MainEvent {
final JsonState jsonState;
final UrlState urlState;
final bool isLoop;
OpenPlayerEvent(this.jsonState, this.urlState, this.isLoop);
}
@@ -0,0 +1,24 @@
part of 'main_bloc.dart';
@immutable
sealed class MainState {
final InputType inputType;
const MainState(this.inputType);
}
class ChosenJsonState extends MainState {
const ChosenJsonState() : super(InputType.json);
}
class ChosenUrlState extends MainState {
const ChosenUrlState() : super(InputType.url);
}
class SettingState extends MainState {
const SettingState(super.inputType);
}
class PlayerState extends MainState {
final Provider provider;
const PlayerState(super.inputType, this.provider);
}
@@ -0,0 +1,86 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
part 'url_event.dart';
part 'url_state.dart';
class UrlBloc extends Bloc<UrlEvent, UrlState> {
UrlBloc() : super(UrlState.create()) {
on<ChooseWayToLoadVideoEvent>(_onChooseWayToGetConfigEvent);
on<ChooseExampleVideoEvent>(_onChooseExampleLinkEvent);
on<ChooseWayToGetLinkEvent>(_onChooseWayToGetLinkEvent);
on<TypeOwnLinkEvent>(_onTypeOwnLinkEvent);
on<ChooseExtraOptionsEvent>(_onChooseExtraOptionsEvent);
on<ChooseConfigFileEvent>(_onChooseConfigFileEvent);
on<ChooseVideoFormatEvent>(_onChoseVideoFormatEvent);
on<TypeOwnLinkWithoutFormatEvent>(_onTypeOwnLinkWithoutFormatEvent);
on<UrlResetEvent>(_onResetEvent);
}
_onResetEvent(UrlResetEvent event, Emitter<UrlState> emit) {
emit(UrlState.create());
}
_onChooseWayToGetConfigEvent(ChooseWayToLoadVideoEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseExampleChosenOptionState.create(parentState, null)
: LoadWithLinkChosenOptionState.create(parentState));
}
_onChooseExampleLinkEvent(ChooseExampleVideoEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseExampleChosenOptionState.create(parentState, event.selectedIndex));
}
_onChooseWayToGetLinkEvent(ChooseWayToGetLinkEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseOwnLinkState.create(parentState, null)
: ChooseExtraOptionsState.create(parentState));
}
_onTypeOwnLinkEvent(TypeOwnLinkEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkState.create(parentState, event.urlPath));
}
_onChooseExtraOptionsEvent(ChooseExtraOptionsEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseOwnLinkWithoutFormat.create(parentState, null, null)
: ChooseJsonFileState.create(parentState, null));
}
_onChooseConfigFileEvent(ChooseConfigFileEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(ChooseJsonFileState.create(parentState, event.selectedIndex));
}
_onChoseVideoFormatEvent(ChooseVideoFormatEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkWithoutFormat.create(parentState, state.urlPath, event.selectedIndex));
}
_onTypeOwnLinkWithoutFormatEvent(TypeOwnLinkWithoutFormatEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkWithoutFormat.create(parentState, event.urlPath, state.currentIndex));
}
}
@@ -0,0 +1,52 @@
part of 'url_bloc.dart';
@immutable
sealed class UrlEvent {}
class UrlResetEvent extends UrlEvent {}
class ChooseWayToLoadVideoEvent extends UrlEvent {
final int selectedIndex;
ChooseWayToLoadVideoEvent(this.selectedIndex);
}
class ChooseExampleVideoEvent extends UrlEvent {
final int selectedIndex;
ChooseExampleVideoEvent(this.selectedIndex);
}
class ChooseWayToGetLinkEvent extends UrlEvent {
final int selectedIndex;
ChooseWayToGetLinkEvent(this.selectedIndex);
}
class TypeOwnLinkEvent extends UrlEvent {
final String urlPath;
TypeOwnLinkEvent(this.urlPath);
}
class ChooseExtraOptionsEvent extends UrlEvent {
final int selectedIndex;
ChooseExtraOptionsEvent(this.selectedIndex);
}
class ChooseVideoFormatEvent extends UrlEvent {
final int selectedIndex;
ChooseVideoFormatEvent(this.selectedIndex);
}
class TypeOwnLinkWithoutFormatEvent extends UrlEvent {
final String urlPath;
TypeOwnLinkWithoutFormatEvent(this.urlPath);
}
class ChooseConfigFileEvent extends UrlEvent {
final int selectedIndex;
ChooseConfigFileEvent(this.selectedIndex);
}
class UpdateOwnLinkWithoutFormatEvent extends UrlEvent {
final String? urlPath;
final int? index;
UpdateOwnLinkWithoutFormatEvent(this.urlPath, this.index);
}
@@ -0,0 +1,345 @@
part of 'url_bloc.dart';
typedef UrlStateIndexedEventCreator = UrlEvent Function(int index);
typedef UrlStateStringEventCreator = UrlEvent Function(String value);
enum UIType { list, input, listWithInput }
/// Состояние по умолчанию
@immutable
class UrlState {
final String title;
final Key key;
final List<OptionData> options;
final String? visibleUrlPath;
final String? urlPath;
final bool? isLive;
final int? initialIndex;
final int? currentIndex;
final UrlState? parent;
final bool isFinal;
final UIType uiType;
final UrlStateIndexedEventCreator? createIndexedEvent;
final UrlStateStringEventCreator? createStringEvent;
const UrlState({
required this.title,
required this.key,
this.options = const [],
this.visibleUrlPath,
this.urlPath,
this.isLive,
this.initialIndex,
this.currentIndex,
this.parent,
this.isFinal = false,
required this.uiType,
this.createIndexedEvent,
this.createStringEvent
});
factory UrlState.create() {
return UrlState(
title: '1. Выберите вариант загрузки видео',
key: const Key('ChooseWayToLoadVideoListID'),
options: const [
OptionData(key: Key('ListElementExampleID'), title: 'Использовать пример'),
OptionData(key: Key('ListElementUrlID'), title: 'Загрузить по ссылке')
],
uiType: UIType.list,
createIndexedEvent: (index) => ChooseWayToLoadVideoEvent(index),
);
}
}
/// Состояние, когда выбран пункт "Использовать пример"
class UseExampleChosenOptionState extends UrlState {
const UseExampleChosenOptionState(
String title,
Key key,
List<OptionData> options,
String? urlPath,
bool? isLive,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory UseExampleChosenOptionState.create(UrlState? parent, int? selectedIndex) {
var options = const [
OptionData(key: Key('ListElementUrl1ID'), title: 'HLS VOD', value: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'),
OptionData(key: Key('ListElementUrl2ID'), title: 'HLS LIVE', value: 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', isLive: true),
OptionData(key: Key('ListElementUrl3ID'), title: 'MP4 (4K)', value: 'https://filesamples.com/samples/video/mp4/sample_3840x2160.mp4'),
OptionData(key: Key('ListElementUrl4ID'), title: 'MP4 (1440p)', value: 'https://filesamples.com/samples/video/mp4/sample_2560x1440.mp4'),
OptionData(key: Key('ListElementUrl5ID'), title: 'MP4 (1080p)', value: 'https://filesamples.com/samples/video/mp4/sample_1920x1080.mp4'),
OptionData(key: Key('ListElementUrl6ID'), title: 'MP4 (720р)', value: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'),
OptionData(key: Key('ListElementUrl7ID'), title: 'MP4 (360p)', value: 'https://filesamples.com/samples/video/mp4/sample_640x360.mp4'),
OptionData(key: Key('ListElementUrl8ID'), title: 'MP4 (144р)', value: 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4'),
OptionData(key: Key('ListElementUrl9ID'), title: 'MP4 (более 10 часов)', value: 'https://cloud.nut.tech/index.php/s/TgFkebRSs3A9i3r/download/Гендальф_10_часовая_версия_Gandalf_10_hour_version.mp4')
];
final String? currentUrlPath;
final bool? isLive;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
isLive = options[selectedIndex].isLive;
} else {
currentUrlPath = null;
isLive = null;
}
return UseExampleChosenOptionState(
'2. Выберите пример видео',
const Key('ChooseWayToGetUrlListID'),
options,
currentUrlPath,
isLive,
0,
selectedIndex,
parent,
(selectedIndex != null),
UIType.list,
(index) => ChooseExampleVideoEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Загрузить по ссылке"
class LoadWithLinkChosenOptionState extends UrlState {
const LoadWithLinkChosenOptionState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
UrlState? parent,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory LoadWithLinkChosenOptionState.create(UrlState? parent) {
return LoadWithLinkChosenOptionState(
'2. Выберите способ получения ссылки',
const Key('ChooseWayToGetLinkListID'),
const [
OptionData(key: Key('ListElementUseUrlID'), title: 'Указать ссылку'),
OptionData(key: Key('ListElementExtraOptionsID'), title: 'Использовать доп. опции')
],
1,
parent,
UIType.list,
(index) => ChooseWayToGetLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Указать ссылку"
class UseOwnLinkState extends UrlState {
const UseOwnLinkState(
String title,
Key key,
String? urlPath,
bool? isLive,
int? initialIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
parent: parent,
isFinal: isFinal,
visibleUrlPath: urlPath,
uiType: uiType,
createStringEvent: createStringEvent
);
factory UseOwnLinkState.create(UrlState? parent, String? urlPath, {bool isLive = false}) {
var finalUrlPath = urlPath ?? 'https://cloud.nut.tech/index.php/s/iY3KtaL7bekWnwM/download/IMG_3059.MP4';
return UseOwnLinkState(
'3. Вставьте ссылку',
const Key('TypeOwnUrlViewID'),
finalUrlPath,
isLive,
0,
parent,
finalUrlPath.isNotEmpty,
UIType.input,
(value) => TypeOwnLinkEvent(value)
);
}
}
/// Состояние, когда выбран пункт "Использовать доп. опции"
class ChooseExtraOptionsState extends UrlState {
const ChooseExtraOptionsState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
UrlState? parent,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ChooseExtraOptionsState.create(UrlState? parent) {
return ChooseExtraOptionsState(
'3. Выберите дополнительные опции',
const Key('ChooseExtraOptionsListID'),
const [
OptionData(key: Key('ListElementNoFormatOptionID'), title: 'Ссылка без расширения'),
OptionData(key: Key('ListElementJSONFileID'), title: 'JSON-файл для MP4')
],
1,
parent,
UIType.list,
(index) => ChooseExtraOptionsEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Ссылка без расширения"
class UseOwnLinkWithoutFormat extends UrlState {
const UseOwnLinkWithoutFormat(
String title,
Key key,
List<OptionData> options,
String? urlPath,
String? visibleUrlPath,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent,
UrlStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
options: options,
urlPath: urlPath,
visibleUrlPath: visibleUrlPath,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent,
createStringEvent: createStringEvent
);
factory UseOwnLinkWithoutFormat.create(UrlState? parent, String? urlPath, int? selectedIndex) {
var options = const [
OptionData(key: Key('ListElementFormat1ID'), title: 'MP4', value: '.mp4'),
OptionData(key: Key('ListElementFormat3ID'), title: 'M3U8', value: '.m3u8')
];
var visibleUrlPath = _removeFormatIfNeeded(urlPath) ?? 'https://cloud.nut.tech/index.php/s/iY3KtaL7bekWnwM/download/IMG_3059';
var currentSelectedIndex = selectedIndex ?? 0;
var finalUrlPath = "$visibleUrlPath${options[currentSelectedIndex].value}";
return UseOwnLinkWithoutFormat(
'4. Вставьте ссылку и выберите формат видео',
const Key('UseOwnLinkWithoutFormatListID'),
options,
finalUrlPath,
visibleUrlPath,
0,
currentSelectedIndex,
parent,
finalUrlPath.isNotEmpty,
UIType.listWithInput,
(index) => ChooseVideoFormatEvent(index),
(value) => TypeOwnLinkWithoutFormatEvent(value)
);
}
static String? _removeFormatIfNeeded(String? urlPath) {
if (urlPath != null && urlPath.endsWith('.mp4')) {
return urlPath.replaceAll(RegExp('.mp4'), '');
} else if (urlPath != null && urlPath.endsWith('.m3u8')) {
return urlPath.replaceAll(RegExp('.m3u8'), '');
} else {
return urlPath;
}
}
}
/// Состояние, когда выбран пункт "JSON-файл для MP4"
class ChooseJsonFileState extends UrlState {
const ChooseJsonFileState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ChooseJsonFileState.create(UrlState? parent, int? selectedIndex) {
return ChooseJsonFileState(
'4. Выберите файл конфигурации',
const Key('ChooseConfigFileListID'),
const [
OptionData(key: Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: Key('ListElementConfig4ID'), title: 'Config 4')
],
1,
selectedIndex,
parent,
// TODO: Пока не реализовано
false,//selectedIndex != null,
UIType.list,
(index) => ChooseConfigFileEvent(index)
);
}
}
@@ -1,11 +1,13 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_view.dart';
import 'package:nut_player_example/src/features/settings_screen/presentation/settings_view.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import '../../../common/repository/settings_repository.dart';
import '../domain/main_bloc/main_bloc.dart';
class Buttons extends StatelessWidget {
final bool _isReady;
const Buttons(this._isReady, {super.key});
const Buttons({super.key});
@override
Widget build(BuildContext context) {
@@ -16,43 +18,45 @@ class Buttons extends StatelessWidget {
// КНОПКА ПОСМОТРЕТЬ ДЕМО
Container(
width: double.infinity,
margin: const EdgeInsets.only(
top: 25, left: 16, right: 16, bottom: 5),
child: CupertinoButton(
key: const Key('WatchDemoButtonID'),
color: _isReady ? CupertinoColors.link : CupertinoColors.systemGrey2,
borderRadius: BorderRadius.circular(12),
onPressed: () {
Navigator.push(context,
CupertinoPageRoute(builder: (BuildContext context) { return PlayerView();})
);
},
child: const Text(
'Посмотреть демо',
textAlign: TextAlign.center,
style: TextStyle(
color: CupertinoColors.white,
fontSize: 17,
fontWeight: FontWeight.w600),
))),
margin: const EdgeInsets.only(top: 25, left: 16, right: 16, bottom: 5),
child: Builder(
builder: (context) {
var urlState = context.watch<UrlBloc>().state;
var jsonState = context.watch<JsonBloc>().state;
return CupertinoButton(
key: const Key('WatchDemoButtonID'),
color: (urlState.isFinal || jsonState.isFinal) ? CupertinoColors.link : CupertinoColors.systemGrey2,
borderRadius: BorderRadius.circular(12),
onPressed: () {
context.read<MainBloc>().add(OpenPlayerEvent(jsonState, urlState, context.read<SettingsRepository>().isLoop));
},
child: const Text(
'Посмотреть демо',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.white,
fontSize: 17,
fontWeight: FontWeight.w600),
));
}
)),
//КНОПКА НАСТРОЙКИ ПЛЕЕРА
Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
margin: const EdgeInsets.symmetric(
vertical: 5, horizontal: 16),
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: CupertinoButton(
key: const Key('PlayerSettingButtonID'),
onPressed: () {
Navigator.push(context,
CupertinoPageRoute(builder: (BuildContext context) { return const SettingsView();})
);
context.read<MainBloc>().add(OpenSettingsEvent());
},
child: const Text(
'Настройки плеера',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.link,
fontSize: 17,
fontWeight: FontWeight.w600),
@@ -1,23 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/main_screen/domain/main_bloc/main_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/buttons.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/json_view.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/url_view.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_view.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
import '../../player_screen/domain/bloc/playerview_bloc.dart';
import '../../settings_screen/presentation/settings_view.dart';
enum InputType { url, json }
class Home extends StatefulWidget {
class Home extends StatelessWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
InputType _inputType = InputType.json;
bool _isReady = false;
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: CupertinoNavigationBar(
@@ -26,78 +29,130 @@ class _HomeState extends State<Home> {
border: null,
backgroundColor: CupertinoColors.white,
),
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Column(
children: [
Container(
key: const Key('MainSegmentedButtonID'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
color: CupertinoColors.white,
child: CupertinoSlidingSegmentedControl<InputType>(
groupValue: _inputType,
onValueChanged: (InputType? value) {
if (value != null) {
setState(() {
_inputType = value;
_isReady = false;
});
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MainBloc()),
BlocProvider(create: (_) => JsonBloc()),
BlocProvider(create: (_) => UrlBloc())
],
child: BlocListener<MainBloc, MainState>(
listener: (context, state) { _navigate(state, context); },
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
return Column(
children: [
Container(
key: const Key('MainSegmentedButtonID'),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
color: CupertinoColors.white,
child: _buildSegmentedButtonWidget(context, state.inputType, (newInputType) {
if (newInputType != null) {
context.read<MainBloc>().add(newInputType == InputType.json
? ChooseJsonEvent()
: ChooseUrlEvent());
newInputType == InputType.url
? context.read<JsonBloc>().add(JsonResetEvent())
: context.read<UrlBloc>().add(UrlResetEvent());
}
})
),
if (state.inputType == InputType.url)
const URLView()
else if (state.inputType == InputType.json)
const JSONView()
else
const Text('unknown')
]
);
},
)
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Builder(
builder: (context) {
return const Buttons();
}
},
children: const <InputType, Widget>{
InputType.json: Padding(
key: Key('JSONSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('JSON-конфиг',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
),
InputType.url: Padding(
key: Key('URLSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Видео URL',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
)
},
),
),
if (_inputType == InputType.json)
JSONView((isReady) {
setState(() {
_isReady = isReady;
});
})
else if (_inputType == InputType.url)
URLView((isReady) {
setState(() {
_isReady = isReady;
});
})
]
)
),
)
),
],
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Buttons(_isReady)
)
),
],
),
),
),
);
}
}
_navigate(MainState state, BuildContext context) {
final repository = context.read<SettingsRepository>();
if (state is PlayerState) {
Navigator.push(context, CupertinoPageRoute(builder: (_) {
return BlocProvider(
create: (BuildContext context) => PlayerViewBloc(provider: state.provider, repository: repository),
child: BlocListener<PlayerViewBloc, PlayerViewState>(
listener: (context, state) {
if (state is PlayerViewDismiss) {
Navigator.of(context).pop();
}
},
child: const PlayerView(),
),
);
}));
} else if (state is SettingState) {
Navigator.push(context, CupertinoPageRoute(builder: (_) {
return BlocProvider(
create: (context) => SettingsBloc(repository),
child: BlocListener<SettingsBloc, SettingsState>(
listener: (context, state) {
if (state is SettingsDismiss) {
Navigator.of(context).pop();
}
},
child: const SettingsView(),
)
);
}));
}
}
_buildSegmentedButtonWidget(BuildContext context, InputType currentInputType, Function(InputType?) onValueChanged) {
return CupertinoSlidingSegmentedControl<InputType>(
groupValue: currentInputType,
onValueChanged: onValueChanged,
children: const <InputType, Widget>{
InputType.json: Padding(
key: Key('JSONSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('JSON-конфиг',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
),
InputType.url: Padding(
key: Key('URLSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Видео URL',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
)
},
);
}
}
@@ -1,117 +1,81 @@
import 'package:flutter/cupertino.dart';
import '../../../common/models/option_data.dart';
import '../../../common/extensions/options_list_extension.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'options_list.dart';
import 'url_input.dart';
class JSONView extends StatefulWidget {
final Function(bool)? isReady;
const JSONView(this.isReady, {super.key});
@override
State<JSONView> createState() => _JSONViewState();
}
class _JSONViewState extends State<JSONView> {
final List<OptionData> _waysToGetConfig = [
OptionData(key: const Key('ListElementConfigLinkID'), title: 'Ссылка на конфиг'),
OptionData(key: const Key('ListElementFirebaseID'), title: 'Firebase'),
];
final List<OptionData> _waysToGetUrlpath = [
OptionData(key: const Key('ListElementUseUrlExampleID'), title: 'Использовать пример ссылки'),
OptionData(key: const Key('ListElementUseOwnUrlID'), title: 'Указать свою')
];
final List<OptionData> _firebaseConfigs = [
OptionData(key: const Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: const Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: const Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: const Key('ListElementConfig4ID'), title: 'Config 4')
];
final List<OptionData> _linksExamples = [
OptionData(key: const Key('ListElementUrl1ID'), title: 'Субтитры_STR'),
OptionData(key: const Key('ListElementUrl2ID'), title: 'Example_2'),
OptionData(key: const Key('ListElementUrl3ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementUrl4ID'), title: 'Example_4'),
OptionData(key: const Key('ListElementUrl5ID'), title: 'Example_5'),
OptionData(key: const Key('ListElementUrl6ID'), title: 'Example_6'),
OptionData(key: const Key('ListElementUrl7ID'), title: 'Example_7'),
OptionData(key: const Key('ListElementUrl8ID'), title: 'Example_8'),
OptionData(key: const Key('ListElementUrl9ID'), title: 'Example_9'),
OptionData(key: const Key('ListElementUrl10ID'), title: 'Example_10')
];
String _currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
class JSONView extends StatelessWidget {
const JSONView({super.key});
@override
Widget build(BuildContext context) {
return Column(children: [
OptionsList(_waysToGetConfig, '1. Выберите способ получения конфига',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetConfig.unselectAll();
_waysToGetConfig[selectedIndex].isSelected = true;
_resetSecondStep();
});
}, key: const Key('ListChooseWayToGetConfigID')),
if (_waysToGetConfig[0].isSelected)
OptionsList(_waysToGetUrlpath, '2. Выберите способ получения ссылки',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetUrlpath.unselectAll();
_waysToGetUrlpath[selectedIndex].isSelected = true;
_resetThirdStep();
});
}, key: const Key('ListChooseWayToGetUrlID'))
else if (_waysToGetConfig[1].isSelected)
OptionsList(
_firebaseConfigs, '2. Выберите файл конфига', ListType.checkmark,
(selectedIndex) {
setState(() {
_firebaseConfigs.unselectAll();
_firebaseConfigs[selectedIndex].isSelected = true;
_resetThirdStep();
widget.isReady!(true);
});
}, key: const Key('ListChooseFbConfigID')),
if (_waysToGetUrlpath[0].isSelected)
OptionsList(
_linksExamples, '3. Выберите пример ссылки', ListType.checkmark,
(selectedIndex) {
setState(() {
_linksExamples.unselectAll();
_linksExamples[selectedIndex].isSelected = true;
widget.isReady!(true);
});
}, key: const Key('ListChooseUrlExampleID'))
else if (_waysToGetUrlpath[1].isSelected)
URLInput('3. Укажите ссылку',
_currentUrlPath, (isURLEmpty) {
setState(() {
widget.isReady!(!isURLEmpty);
});
}),
]);
return BlocBuilder<JsonBloc, JsonState>(
builder: (context, state) {
return Column(
children: _buildAllWidgets(context, state, null)
);
},
);
}
void _resetSecondStep() {
setState(() {
_firebaseConfigs.unselectAll();
_waysToGetUrlpath.unselectAll();
_resetThirdStep();
});
List<Widget> _buildAllWidgets(BuildContext context, JsonState state, int? index) {
List<Widget> widgets = [];
final parent = state.parent;
if (parent != null) {
widgets.addAll(_buildAllWidgets(context, parent, state.initialIndex));
}
var widget = _buildWidget(state, context, index);
if (widget != null) {
widgets.removeWhere((element) => element.key == widget.key);
widgets.add(widget);
}
return widgets;
}
void _resetThirdStep() {
setState(() {
_linksExamples.unselectAll();
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
if (widget.isReady != null) {
widget.isReady!(false);
}
});
Widget? _buildWidget(JsonState state, BuildContext context, int? index) {
if (state.uiType == UIType.input) {
return URLInput(
state.title,
state.urlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<JsonBloc>().add(createString(value));
}
},
key: state.key,
);
} else if (state.uiType == UIType.list) {
if (state.options.isEmpty) { return _buildLoader(); }
var isListCheckmarked = state.options.length > 2;
return OptionsList(
state.options,
isListCheckmarked ? state.currentIndex : index,
state.title,
isListCheckmarked ? ListType.checkmark : ListType.radio,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<JsonBloc>().add(createIndexed(selectedIndex));
}
},
key: state.key
);
} else {
return null;
}
}
Widget _buildLoader() {
return Container(
margin: const EdgeInsets.all(10),
width: double.infinity,
height: 80,
child: const CupertinoActivityIndicator(radius: 20),
);
}
}
@@ -1,5 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/views/checkmark_list_option.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/radio_list_option.dart';
import '../../../common/models/option_data.dart';
@@ -8,11 +10,12 @@ enum ListType { radio, checkmark }
class OptionsList extends StatelessWidget {
final List<OptionData> options;
final int? selectedOptionIndex;
final String? title;
final ListType type;
final Function(int)? onSelection;
const OptionsList(this.options, this.title, this.type, this.onSelection, {super.key});
const OptionsList(this.options, this.selectedOptionIndex, this.title, this.type, this.onSelection, {super.key});
@override
Widget build(BuildContext context) {
@@ -27,6 +30,7 @@ class OptionsList extends StatelessWidget {
Text(title!,
textAlign: TextAlign.right,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
fontWeight: FontWeight.w500,
color: CupertinoColors.black)),
@@ -47,24 +51,32 @@ class OptionsList extends StatelessWidget {
color: CupertinoColors.separator);
},
itemBuilder: (_, int newIndex) {
if (type == ListType.radio) {
return RadioListOption(options[newIndex], () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else if (type == ListType.checkmark) {
return CheckmarkListOption(options[newIndex], () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else {
return null;
}
return BlocBuilder<JsonBloc, JsonState>(
builder: (context, state) {
return _buildListOptionWidget(type, newIndex, selectedOptionIndex, context.read<JsonBloc>());
},
);
}))
],
),
);
}
Widget _buildListOptionWidget(ListType type, int newIndex, int? selectedIndex, JsonBloc bloc) {
if (type == ListType.radio) {
return RadioListOption(options[newIndex], newIndex == selectedIndex, () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else if (type == ListType.checkmark) {
return CheckmarkListOption(options[newIndex].title, newIndex == selectedIndex, () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else {
return const Text('unknown');
}
}
}
@@ -3,9 +3,10 @@ import 'package:nut_player_example/src/common/models/option_data.dart';
class RadioListOption extends StatelessWidget {
final OptionData data;
final bool isSelected;
final Function? onTap;
const RadioListOption(this.data, this.onTap, {super.key});
const RadioListOption(this.data, this.isSelected, this.onTap, {super.key});
@override
Widget build(BuildContext context) {
@@ -18,10 +19,10 @@ class RadioListOption extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(color: CupertinoColors.link, width: 2.5),
color: data.isSelected ? CupertinoColors.link : CupertinoColors.white
color: isSelected ? CupertinoColors.link : CupertinoColors.white
),
),
title: Text(data.title, style: const TextStyle(fontSize: 17))
title: Text(data.title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
);
}
}
@@ -1,29 +1,27 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
class URLInput extends StatefulWidget {
class URLInput extends StatelessWidget {
final String title;
final String placeholder;
final Function(bool)? isTextFieldEmpty;
final String initialText;
final Function(String)? newText;
const URLInput(this.title, this.placeholder, this.isTextFieldEmpty, {super.key});
URLInput(this.title, this.initialText, this.newText, {super.key});
@override
State<URLInput> createState() => _URLInputState();
}
class _URLInputState extends State<URLInput> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
_controller.text = initialText;
return Container(
margin: const EdgeInsets.only(top: 20, left: 16, right: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.title,
Text(title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
fontWeight: FontWeight.w500,
color: CupertinoColors.black)),
@@ -50,6 +48,7 @@ class _URLInputState extends State<URLInput> {
onPressed: _getClipboardText,
child: const Text('Вставка',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
fontWeight: FontWeight.w500,
color: CupertinoColors.link))),
@@ -57,6 +56,7 @@ class _URLInputState extends State<URLInput> {
onPressed: _cleanTextField,
child: const Text('Очистить',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
fontWeight: FontWeight.w500,
color: CupertinoColors.link)))
@@ -69,20 +69,12 @@ class _URLInputState extends State<URLInput> {
void _getClipboardText() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
setState(() {
_controller.text = clipboardData?.text ?? '';
if (widget.isTextFieldEmpty != null) {
widget.isTextFieldEmpty!(_controller.text.isEmpty);
}
});
_controller.text = clipboardData?.text ?? '';
newText?.call(_controller.text);
}
void _cleanTextField() async {
setState(() {
_controller.text = '';
if (widget.isTextFieldEmpty != null) {
widget.isTextFieldEmpty!(true);
}
});
_controller.text = '';
newText?.call('');
}
}
@@ -1,169 +1,108 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/options_list.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/url_input.dart';
import '../../../common/extensions/options_list_extension.dart';
import '../../../common/models/option_data.dart';
class URLView extends StatefulWidget {
final Function(bool)? isReady;
class URLView extends StatelessWidget {
const URLView(this.isReady, {super.key});
@override
State<URLView> createState() => _URLViewState();
}
class _URLViewState extends State<URLView> {
final List<OptionData> _waysToDownloadVideo = [
OptionData(key: const Key('ListElementExampleID'), title: 'Использовать пример'),
OptionData(key: const Key('ListElementUrlID'), title: 'Загрузить по ссылке'),
];
final List<OptionData> _videoExamples = [
OptionData(key: const Key('ListElementUrl1ID'), title: 'MP4'),
OptionData(key: const Key('ListElementUrl2ID'), title: 'Example_2'),
OptionData(key: const Key('ListElementUrl3ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementUrl4ID'), title: 'MOV'),
OptionData(key: const Key('ListElementUrl5ID'), title: 'Example_5'),
OptionData(key: const Key('ListElementUrl6ID'), title: 'AVI'),
OptionData(key: const Key('ListElementUrl7ID'), title: 'MKV')
];
final List<OptionData> _waysToGetUrl = [
OptionData(key: const Key('ListElementUseUrlID'), title: 'Указать ссылку'),
OptionData(key: const Key('ListElementExtraOptionsID'), title: 'Использовать доп. опции')
];
final List<OptionData> _extraOptions = [
OptionData(key: const Key('ListElementNoFormatOptionID'), title: 'Ссылка без расширения'),
OptionData(key: const Key('ListElementJSONFileID'), title: 'JSON-файл для MP4'),
];
final List<OptionData> _formats = [
OptionData(key: const Key('ListElementFormat1ID'), title: 'MP4'),
OptionData(key: const Key('ListElementFormat2ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementFormat3ID'), title: 'MOV'),
OptionData(key: const Key('ListElementFormat4ID'), title: 'MKV')
];
final List<OptionData> _configs = [
OptionData(key: const Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: const Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: const Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: const Key('ListElementConfig4ID'), title: 'Config 4')
];
String _currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
bool _isURLCorrect = false;
bool _isFormatSelected = false;
const URLView({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
OptionsList(_waysToDownloadVideo, '1. Выберите вариант загрузки видео',
ListType.radio, (selectedIndex) {
_waysToDownloadVideo.unselectAll();
_waysToDownloadVideo[selectedIndex].isSelected = true;
_resetSecondStep();
}, key: const Key('ListChooseDownloadVideoOption')),
if (_waysToDownloadVideo[0].isSelected)
OptionsList(_videoExamples, '2. Выберите пример видео',
ListType.checkmark, (selectedIndex) {
setState(() {
_videoExamples.unselectAll();
_videoExamples[selectedIndex].isSelected = true;
_resetThirdStep();
if (widget.isReady != null) {
widget.isReady!(true);
}
});
}, key: const Key('ListChooseWayToGetUrlID'))
else if (_waysToDownloadVideo[1].isSelected)
OptionsList(
_waysToGetUrl, '2. Выберите способ получения ссылки',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetUrl.unselectAll();
_waysToGetUrl[selectedIndex].isSelected = true;
_resetThirdStep();
});
}, key: const Key('ListChooseFbConfigID')),
if (_waysToGetUrl[0].isSelected)
URLInput('3. Вставьте ссылку',
_currentUrlPath, (isURLEmpty) {
_resetFourthStep();
if (widget.isReady != null) {
widget.isReady!(!isURLEmpty);
}
})
else if (_waysToGetUrl[1].isSelected)
OptionsList(_extraOptions, '3. Выберите дополнительные опции',
ListType.radio, (selectedIndex) {
setState(() {
_extraOptions.unselectAll();
_extraOptions[selectedIndex].isSelected = true;
_resetFourthStep();
});
}),
if (_extraOptions[0].isSelected)
Column(
children: [
URLInput('4. Вставьте ссылку и выберите формат видео', _currentUrlPath, (isURLEmpty) {
_isURLCorrect = !isURLEmpty;
if (widget.isReady != null) {
widget.isReady!(_isURLCorrect && _isFormatSelected);
}
}),
OptionsList(_formats, null, ListType.checkmark, (selectedIndex) {
setState(() {
_formats.unselectAll();
_formats[selectedIndex].isSelected = true;
_isFormatSelected = true;
if (widget.isReady != null) {
widget.isReady!(_isURLCorrect && _isFormatSelected);
}
});
})
]
)
else if (_extraOptions[1].isSelected)
OptionsList(_configs, '4. Выберите файл конфигурации', ListType.checkmark, (selectedIndex) {
setState(() {
_configs.unselectAll();
_configs[selectedIndex].isSelected = true;
if (widget.isReady != null) {
widget.isReady!(true);
}
});
})
]
return BlocBuilder<UrlBloc, UrlState>(
builder: (context, state) {
return Column(
children: _buildAllWidgets(context, state, null)
);
},
);
}
void _resetSecondStep() {
setState(() {
_videoExamples.unselectAll();
_waysToGetUrl.unselectAll();
_resetThirdStep();
});
List<Widget> _buildAllWidgets(BuildContext context, UrlState state, int? index) {
List<Widget> widgets = [];
final parent = state.parent;
if (parent != null) {
widgets.addAll(_buildAllWidgets(context, parent, state.currentIndex ?? state.initialIndex));
}
var newWidget = _buildWidget(state, context, index);
if (newWidget != null) {
widgets.removeWhere((element) => element.key == newWidget.key);
widgets.add(newWidget);
}
return widgets;
}
void _resetThirdStep() {
setState(() {
_extraOptions.unselectAll();
_resetFourthStep();
});
Widget? _buildWidget(UrlState state, BuildContext context, int? index) {
if (state.uiType == UIType.listWithInput) {
return _buildComplexWidget(state, context);
} else if (state.uiType == UIType.input) {
return URLInput(
state.title,
state.visibleUrlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<UrlBloc>().add(createString(value));
}
},
key: state.key,
);
} else if (state.uiType == UIType.list) {
var isListCheckmarked = state.options.length > 2;
return OptionsList(
state.options,
isListCheckmarked ? state.currentIndex : index,
state.title,
isListCheckmarked ? ListType.checkmark : ListType.radio,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<UrlBloc>().add(createIndexed(selectedIndex));
}
},
key: state.key
);
} else {
return null;
}
}
void _resetFourthStep() {
setState(() {
_configs.unselectAll();
_formats.unselectAll();
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
if (widget.isReady != null) {
widget.isReady!(false);
}
});
Widget? _buildComplexWidget(UrlState state, BuildContext context) {
if (state.visibleUrlPath == null) { return null; }
return Column(
children: [
URLInput(
state.title,
state.visibleUrlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<UrlBloc>().add(createString(value));
}
},
key: state.key,
),
OptionsList(
state.options,
state.currentIndex,
null,
ListType.checkmark,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<UrlBloc>().add(createIndexed(selectedIndex));
}
},
),
],
);
}
}
@@ -0,0 +1,264 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/player_screen/domain/model/subtitle.dart';
import 'package:nut_player_example/src/features/player_screen/domain/model/video_quality.dart';
import 'package:nut_player_example/src/features/player_screen/mapper/settings_mapper.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
part 'playerview_event.dart';
part 'playerview_state.dart';
class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
final VideoPlayerController controller;
late SettingsRepository startSettings;
late VoidCallback? _subtitlesListener;
late VoidCallback? _qualitiesListener;
late VoidCallback? _isPlayingListener;
PlayerViewBloc({required Provider provider, required SettingsRepository repository}):
controller = VideoPlayerController.provider(provider)
..initialize(params: {'enablePip': repository.isPipAvailable,
'enableAutostart': repository.isAutostart,
'enableSettings': repository.isSettingsAvailable,
'enableFullscreen': repository.fullscreenSettings != RepositoryFullscreenSettings.off,
'isSkinByDefault': repository.isSkinByDefault,
'startPosition': repository.start,
'enableLoop': repository.isLoop,
'quality': SettingsMapper.qualityDictionary(repository.quality),
'qualityNaming': repository.qualityNaming.name,
'timeouts': {
'playlist': repository.playlist,
'track': repository.track,
'chunk': repository.chunk
}}),
super(PlayerViewController.createFromRepository(repository)) {
startSettings = repository;
_onInitialize(repository);
on<DismissEvent>(_onDismissEvent);
on<PlayEvent>(_onPlayEvent);
on<PauseEvent>(_onPauseEvent);
on<EndEvent>(_onEndEvent);
on<SeekEvent>(_onSeekEvent);
on<SpeedChangedEvent>(_onSpeedChangedEvent);
on<VolumeChangedEvent>(_onVolumeChangedEvent);
on<QualityChangedEvent>(_onQualityChangedEvent);
on<SubsChangedEvent>(_onSubsChangedEvent);
on<StartPositionChangedEvent>(_onStartPositionChangedEvent);
on<SubsReceivedEvent>(_onSubsReceivedEvent);
on<QualitiesReceivedEvent>(_onQualitiesReceivedEvent);
}
_onInitialize(SettingsRepository repository) {
controller.setLog(repository.log.key);
controller.setVolume(repository.volume);
controller.setBrightness(repository.brightness);
_isPlayingListener = () {
final isPlaying = controller.value.isPlaying;
if (isPlaying) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
controller.setPlaybackSpeed(playerState.speed);
_listenToChanges(_isPlayingListener, false);
}
};
_listenToChanges(_isPlayingListener, true);
_qualitiesListener = () {
final qualities = controller.value.qualities;
if (qualities != null && qualities.isNotEmpty) {
add(QualitiesReceivedEvent(qualities));
_listenToChanges(_qualitiesListener, false);
_qualitiesListener = null;
}
};
_listenToChanges(_qualitiesListener, true);
if (!repository.isSubtitlesAvailable) { return; }
_subtitlesListener = () {
final subtitles = controller.value.subtitles;
if (subtitles != null) {
if (subtitles.length > 1) { add(SubsReceivedEvent(subtitles)); }
_listenToChanges(_subtitlesListener, false);
_subtitlesListener = null;
}
};
_listenToChanges(_subtitlesListener, true);
}
_listenToChanges(VoidCallback? listener, bool listen) {
if (listener != null) {
listen ? controller.addListener(listener) : controller.removeListener(listener);
}
}
_onPlayEvent(PlayEvent event, Emitter<PlayerViewState> emit) {
controller.play();
}
_onPauseEvent(PauseEvent event, Emitter<PlayerViewState> emit) {
controller.pause();
}
_onEndEvent(EndEvent event, Emitter<PlayerViewState> emit) {
controller.end();
}
_onSeekEvent(SeekEvent event, Emitter<PlayerViewState> emit) async {
final seekTime = event.time;
controller.seek(Duration(seconds: seekTime.toInt()));
}
_onDismissEvent(DismissEvent event, Emitter<PlayerViewState> emit) {
emit(PlayerViewDismiss());
}
_onQualitiesReceivedEvent(QualitiesReceivedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final List<VideoQualityOption> mappedQualities = event.qualities.map((e) =>
VideoQualityOption(
id: e["id"],
bandwidth: e["bandwidth"],
width: e["width"],
height: e["height"],
title: e["title"]
)
).toList();
final settingsQuality = startSettings.quality;
final presetQuality = mappedQualities.firstWhere((element) =>
element.quality.identifier == settingsQuality.identifier,
orElse: () => mappedQualities.first);
var sortedQualities = mappedQualities..sort((e1, e2) => e1.height.compareTo(e2.height));
emit(playerState.copy(qualities: sortedQualities, quality: presetQuality));
}
_onSubsReceivedEvent(SubsReceivedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
var subtitles = event.subs.entries.map((element) => Subtitle(id: element.key, title: element.value)).toList();
final offSubtitle = subtitles.firstWhere((element) => element.title == "Settings.Subtitles.off");
final newOffSubtitle = Subtitle(id: offSubtitle.id, title: "Выкл.");
subtitles.removeWhere((element) => element.title == "Settings.Subtitles.off");
var sortedSubtitles = subtitles..sort((e1, e2) => e1.title.compareTo(e2.title));
sortedSubtitles.insert(0, newOffSubtitle);
emit(playerState.copy(subtitles: sortedSubtitles, currentSubtitle: sortedSubtitles.first));
}
_onSubsChangedEvent(SubsChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final currentSub = playerState.subtitles?.firstWhere((element) => event.option.value == element.id);
if (currentSub == null) { return; }
controller.setSubtitles(currentSub.id);
emit(playerState.copy(currentSubtitle: currentSub));
}
_onSpeedChangedEvent(SpeedChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final speedWithoutX = event.option.title.replaceAll(RegExp('x'), '');
final speed = double.tryParse(speedWithoutX) ?? 1;
controller.setPlaybackSpeed(speed);
emit(playerState.copy(speed: speed));
}
_onVolumeChangedEvent(VolumeChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final volume = double.parse(event.option.title);
controller.setVolume(volume);
emit(playerState.copy(volume: volume));
}
_onStartPositionChangedEvent(StartPositionChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final duration = Duration(seconds: event.value);
controller.seek(duration);
emit(playerState.copy(start: event.value));
}
_onQualityChangedEvent(QualityChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final currentQuality = playerState.qualities?.firstWhere((element) => event.option.value == element.id);
if (currentQuality == null) { return; }
controller.setQuality(currentQuality.id);
emit(playerState.copy(quality: currentQuality));
}
int currentNumericValue(NumericOptionData setting) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
if (setting.key == const Key('PlaybackOptionStartPositionID')) {
return playerState.start ?? 0;
} else {
return 0;
}
}
int selectedIndex(OptionDataContainer setting) {
if (setting.key == const Key('PlaybackVolumeSettingID')) {
return _selectedIndexForVolume(setting.options);
} else if (setting.key == const Key('PlaybackSpeedSettingID')) {
return _selectedIndexForSpeed(setting.options);
} else if (setting.key == const Key('PlaybackSubsSettingID')) {
return _selectedIndexForSubtitle(setting.options);
} else if (setting.key == const Key('PlaybackQualitySettingID')) {
return _selectedIndexForQuality(setting.options);
} else {
return 0;
}
}
int _selectedIndexForSpeed(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 3; }
return settings.indexWhere((element) => element.value == playerState.speed.toString());
}
int _selectedIndexForVolume(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 3; }
return settings.indexWhere((element) => element.value == playerState.volume.toString());
}
int _selectedIndexForSubtitle(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
return settings.indexWhere((element) => element.value == playerState.currentSubtitle?.id);
}
int _selectedIndexForQuality(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
return settings.indexWhere((element) => element.value == playerState.quality?.id);
}
}
@@ -0,0 +1,67 @@
part of 'playerview_bloc.dart';
@immutable
sealed class PlayerViewEvent {
const PlayerViewEvent();
}
class DismissEvent extends PlayerViewEvent {
const DismissEvent();
}
class PlayEvent extends PlayerViewEvent {
const PlayEvent();
}
class PauseEvent extends PlayerViewEvent {
const PauseEvent();
}
class EndEvent extends PlayerViewEvent {
const EndEvent();
}
class SeekEvent extends PlayerViewEvent {
final double time;
const SeekEvent(this.time);
}
class SpeedChangedEvent extends PlayerViewEvent {
final OptionData option;
const SpeedChangedEvent(this.option);
}
class VolumeChangedEvent extends PlayerViewEvent {
final OptionData option;
const VolumeChangedEvent(this.option);
}
class QualityChangedEvent extends PlayerViewEvent {
final OptionData option;
const QualityChangedEvent(this.option);
}
class SubsReceivedEvent extends PlayerViewEvent {
final Map<String, String> subs;
const SubsReceivedEvent(this.subs);
}
class QualitiesReceivedEvent extends PlayerViewEvent {
final List<Map<String, dynamic>> qualities;
const QualitiesReceivedEvent(this.qualities);
}
class SubsChangedEvent extends PlayerViewEvent {
final OptionData option;
const SubsChangedEvent(this.option);
}
class FullscreenChangedEvent extends PlayerViewEvent {
final OptionData option;
const FullscreenChangedEvent(this.option);
}
class StartPositionChangedEvent extends PlayerViewEvent {
final int value;
const StartPositionChangedEvent(this.value);
}
@@ -0,0 +1,142 @@
part of 'playerview_bloc.dart';
sealed class PlayerViewState {}
class PlayerViewDismiss extends PlayerViewState {}
@immutable
class PlayerViewController extends PlayerViewState {
final double volume;
final double speed;
final int? start;
final VideoQualityOption? quality;
final List<Subtitle>? subtitles;
final List<VideoQualityOption>? qualities;
final Subtitle? currentSubtitle;
PlayerViewController(
{required this.volume,
required this.speed,
this.start,
this.quality,
this.subtitles,
this.qualities,
this.currentSubtitle});
PlayerViewController copy(
{double? volume,
double? speed,
int? start,
VideoQualityOption? quality,
List<Subtitle>? subtitles,
List<VideoQualityOption>? qualities,
Subtitle? currentSubtitle}) {
return PlayerViewController(
volume: volume ?? this.volume,
speed: speed ?? this.speed,
start: start ?? this.start,
quality: quality ?? this.quality,
subtitles: subtitles ?? this.subtitles,
qualities: qualities ?? this.qualities,
currentSubtitle: currentSubtitle ?? this.currentSubtitle
);
}
factory PlayerViewController.createFromRepository(
SettingsRepository repository) {
return PlayerViewController(
volume: repository.volume,
speed: repository.speed,
start: repository.start);
}
static final playbackSettings = [
OptionDataContainer(
key: const Key('PlaybackVolumeSettingID'),
title: 'Громкость',
options: const [
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume00ID'), '0.0'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume01ID'), '0.1'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume02ID'), '0.2'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume03ID'), '0.3'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume04ID'), '0.4'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume05ID'), '0.5'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume06ID'), '0.6'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume07ID'), '0.7'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume08ID'), '0.8'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume10ID'), '1.0')
],
selectedIndex: 4,
onSelectedOption: (option) => VolumeChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackSpeedSettingID'),
title: 'Скорость',
options: const [
OptionData(key: Key('PlaybackOptionSpeed025xID'), title: '0.25x', value: '0.25'),
OptionData(key: Key('PlaybackOptionSpeed05xID'), title: '0.5x', value: '0.5'),
OptionData(key: Key('PlaybackOptionSpeed075xID'), title: '0.75x', value: '0.75'),
OptionData(key: Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', value: '1.0'),
OptionData(key: Key('PlaybackOptionSpeed125xID'), title: '1.25x', value: '1.25'),
OptionData(key: Key('PlaybackOptionSpeed15xID'), title: '1.5x', value: '1.5'),
OptionData(key: Key('PlaybackOptionSpeed175xID'), title: '1.75x', value: '1.75'),
OptionData(key: Key('PlaybackOptionSpeed2xID'), title: '2x', value: '2.0')
],
selectedIndex: 3,
onSelectedOption: (option) => SpeedChangedEvent(option)
)
];
static final startPosition = NumericOptionData(
key: const Key('PlaybackOptionStartPositionID'),
title: 'Стартовая позиция',
value: 0,
onChange: (newValue) => StartPositionChangedEvent(newValue)
);
static OptionDataContainer? createSubs(List<Subtitle>? subs) {
if (subs == null) { return null; }
final options = subs.map((option) => OptionData(
key: Key(option.id),
title: option.title,
value: option.id
)).toList();
return OptionDataContainer(
key: const Key('PlaybackSubsSettingID'),
title: 'Субтитры',
options: options,
selectedIndex: 0,
onSelectedOption: (option) => SubsChangedEvent(option)
);
}
static OptionDataContainer? createQualities(List<VideoQualityOption>? qualities) {
if (qualities == null) { return null; }
final options = qualities.map((option) => OptionData(
key: Key(option.id),
title: option.title,
value: option.id
)).toList();
return OptionDataContainer(
key: const Key('PlaybackQualitySettingID'),
title: 'Качество',
options: options,
selectedIndex: 0,
onSelectedOption: (option) => QualityChangedEvent(option)
);
}
}
@@ -0,0 +1,83 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
part 'custom_skin_event.dart';
part 'custom_skin_state.dart';
class CustomSkinBloc extends Bloc<CustomSkinEvent, CustomSkinState> {
final PlayerViewBloc _bloc;
CustomSkinBloc(this._bloc): super(CustomSkin.create()) {
_onInitialize();
on<ValueChanged>(_onValueChanged);
on<SkinVisibilityChanged>(_onSkinVisibilityChanged);
on<CurrentPositionChanged>(_onCurrentPositionChanged);
on<PlaybackChanged>(_onPlaybackChanged);
on<PlayerSeekEvent>(_onPlayerSeekEvent);
on<PlayerSeekBackEvent>(_onPlayerSeekBackEvent);
on<PlayerSeekForwardEvent>(_onPlayerSeekForwardEvent);
}
_onInitialize() {
_bloc.controller.addListener(() {
add(ValueChanged(
_bloc.controller.value.isPlaying,
_bloc.controller.value.duration.inSeconds.toDouble(),
_bloc.controller.value.position.inSeconds.toDouble()
));
});
}
_onValueChanged(ValueChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(
isPlaying: event.isPlaying,
duration: event.duration,
currentPosition: event.time
));
}
_onSkinVisibilityChanged(SkinVisibilityChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(isVisible: !skinState.isVisible));
}
_onCurrentPositionChanged(CurrentPositionChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(currentPosition: event.time));
}
_onPlaybackChanged(PlaybackChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
_bloc.add(skinState.isPlaying ? const PauseEvent() : const PlayEvent());
}
_onPlayerSeekBackEvent(PlayerSeekBackEvent event, Emitter<CustomSkinState> emit) async {
final currentTime = await _bloc.controller.position;
if (currentTime != null) {
final seekTime = currentTime.inSeconds - 15;
_bloc.add(SeekEvent(seekTime.toDouble()));
}
}
_onPlayerSeekForwardEvent(PlayerSeekForwardEvent event, Emitter<CustomSkinState> emit) async {
final currentTime = await _bloc.controller.position;
if (currentTime != null) {
final seekTime = currentTime.inSeconds + 15;
_bloc.add(SeekEvent(seekTime.toDouble()));
}
}
_onPlayerSeekEvent(PlayerSeekEvent event, Emitter<CustomSkinState> emit) {
_bloc.add(SeekEvent(event.time));
}
}
@@ -0,0 +1,38 @@
part of 'custom_skin_bloc.dart';
@immutable
sealed class CustomSkinEvent {}
class SkinVisibilityChanged extends CustomSkinEvent {
SkinVisibilityChanged();
}
class ValueChanged extends CustomSkinEvent {
final bool isPlaying;
final double duration;
final double time;
ValueChanged(this.isPlaying, this.duration, this.time);
}
class PlaybackChanged extends CustomSkinEvent {
PlaybackChanged();
}
class CurrentPositionChanged extends CustomSkinEvent {
final double time;
CurrentPositionChanged(this.time);
}
class PlayerSeekBackEvent extends CustomSkinEvent {
PlayerSeekBackEvent();
}
class PlayerSeekForwardEvent extends CustomSkinEvent {
PlayerSeekForwardEvent();
}
class PlayerSeekEvent extends CustomSkinEvent {
final double time;
PlayerSeekEvent(this.time);
}
@@ -0,0 +1,31 @@
part of 'custom_skin_bloc.dart';
@immutable
sealed class CustomSkinState {}
class CustomSkin extends CustomSkinState {
final bool isVisible;
final bool isPlaying;
final double duration;
final double currentPosition;
CustomSkin({required this.isVisible, required this.isPlaying, required this.duration, required this.currentPosition});
CustomSkin copy({bool? isVisible, bool? isPlaying, double? duration, double? currentPosition}) {
return CustomSkin(
isVisible: isVisible ?? this.isVisible,
isPlaying: isPlaying ?? this.isPlaying,
duration: duration ?? this.duration,
currentPosition: currentPosition ?? this.currentPosition
);
}
factory CustomSkin.create() {
return CustomSkin(
isVisible: true,
isPlaying: false,
duration: 0,
currentPosition: 0
);
}
}
@@ -0,0 +1,6 @@
class Subtitle {
final String id;
final String title;
Subtitle({required this.id, required this.title});
}
@@ -0,0 +1,73 @@
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
class VideoQualityOption {
String id;
int bandwidth;
int width;
int height;
String title;
late VideoQuality quality;
VideoQualityOption({
required this.id,
required this.bandwidth,
required this.width,
required this.height,
required this.title
}) {
quality = _videoQuality(width,height,bandwidth);
}
static VideoQuality _qualityFromResolution(int pixels) {
if (pixels >= 1000 && pixels < 90500) {
return VideoQuality.p144;
} else if (pixels >= 90500 && pixels < 170500) {
return VideoQuality.p240;
} else if (pixels >= 170500 && pixels < 280500) {
return VideoQuality.p360;
} else if (pixels >= 280500 && pixels < 640500) {
return VideoQuality.p480;
} else if (pixels >= 640500 && pixels < 1500500) {
return VideoQuality.p720;
} else if (pixels >= 1500500 && pixels < 2400500) {
return VideoQuality.p1080;
} else if (pixels >= 2400500 && pixels < 6000500) {
return VideoQuality.p1440;
} else if (pixels >= 6000500) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _qualityFromBandwidth(int bandwidth) {
if (bandwidth >= 1 && bandwidth < 400001) {
return VideoQuality.p144;
} else if (bandwidth >= 400001 && bandwidth < 800001) {
return VideoQuality.p240;
} else if (bandwidth >= 800001 && bandwidth < 1200001) {
return VideoQuality.p360;
} else if (bandwidth >= 1200001 && bandwidth < 1800001) {
return VideoQuality.p480;
} else if (bandwidth >= 1800001 && bandwidth < 3500001) {
return VideoQuality.p720;
} else if (bandwidth >= 3500001 && bandwidth < 8000001) {
return VideoQuality.p1080;
} else if (bandwidth >= 8000001 && bandwidth < 12000001) {
return VideoQuality.p1440;
} else if (bandwidth >= 12000001) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _videoQuality(int width, int height, int bandwidth) {
if (width != 0 && height != 0) {
final pixels = width * height;
return _qualityFromResolution(pixels);
} else {
return _qualityFromBandwidth(bandwidth);
}
}
}
@@ -0,0 +1,157 @@
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
class SettingsMapper {
static RepositoryFullscreenSettings repoFullscreenSettings(FullscreenSettings setting) {
switch (setting) {
case FullscreenSettings.landscape:
return RepositoryFullscreenSettings.landscape;
case FullscreenSettings.flexible:
return RepositoryFullscreenSettings.flexible;
case FullscreenSettings.off:
return RepositoryFullscreenSettings.off;
}
}
static RepositoryVideoQualityNaming repoQualityNaming(String naming) {
switch (naming) {
case "common":
return RepositoryVideoQualityNaming.common;
case "rus":
return RepositoryVideoQualityNaming.rus;
case "eng":
return RepositoryVideoQualityNaming.eng;
case "resolution":
return RepositoryVideoQualityNaming.resolution;
default:
return RepositoryVideoQualityNaming.common;
}
}
static VideoQualityNaming qualityNaming(RepositoryVideoQualityNaming naming) {
switch (naming) {
case RepositoryVideoQualityNaming.common:
return VideoQualityNaming.common;
case RepositoryVideoQualityNaming.rus:
return VideoQualityNaming.rus;
case RepositoryVideoQualityNaming.eng:
return VideoQualityNaming.eng;
case RepositoryVideoQualityNaming.resolution:
return VideoQualityNaming.resolution;
default:
return VideoQualityNaming.common;
}
}
static RepositoryVideoQuality repoVideoQuality(String setting) {
switch (setting) {
case "auto":
return RepositoryVideoQuality.auto;
case "p144":
return RepositoryVideoQuality.p144;
case "p240":
return RepositoryVideoQuality.p240;
case "p360":
return RepositoryVideoQuality.p360;
case "p480":
return RepositoryVideoQuality.p480;
case "p720":
return RepositoryVideoQuality.p720;
case "p1080":
return RepositoryVideoQuality.p1080;
case "p1440":
return RepositoryVideoQuality.p1440;
case "p2160":
return RepositoryVideoQuality.p2160;
default:
return RepositoryVideoQuality.auto;
}
}
static VideoQuality quality(RepositoryVideoQuality videoQuality) {
switch (videoQuality) {
case RepositoryVideoQuality.auto:
return VideoQuality.auto;
case RepositoryVideoQuality.p2160:
return VideoQuality.p2160;
case RepositoryVideoQuality.p1440:
return VideoQuality.p1440;
case RepositoryVideoQuality.p1080:
return VideoQuality.p1080;
case RepositoryVideoQuality.p720:
return VideoQuality.p720;
case RepositoryVideoQuality.p480:
return VideoQuality.p480;
case RepositoryVideoQuality.p360:
return VideoQuality.p360;
case RepositoryVideoQuality.p240:
return VideoQuality.p240;
case RepositoryVideoQuality.p144:
return VideoQuality.p144;
}
}
static FullscreenSettings fullscreenSetting(RepositoryFullscreenSettings settings) {
switch (settings) {
case RepositoryFullscreenSettings.landscape:
return FullscreenSettings.landscape;
case RepositoryFullscreenSettings.flexible:
return FullscreenSettings.flexible;
case RepositoryFullscreenSettings.off:
return FullscreenSettings.off;
}
}
static SkinColor skinColor(RepositorySkinColor skinColor) {
switch (skinColor) {
case RepositorySkinColor.white:
return SkinColor.white;
}
}
static LogType logType(RepositoryLogType logType) {
switch (logType) {
case RepositoryLogType.off:
return LogType.off;
case RepositoryLogType.info:
return LogType.info;
case RepositoryLogType.debug:
return LogType.debug;
}
}
static Map<String, dynamic> qualityDictionary(RepositoryVideoQuality quality) {
switch (quality) {
case RepositoryVideoQuality.auto:
return _createQualityMap(bandwidth: 0, width: 0, height: 0);
case RepositoryVideoQuality.p2160:
return _createQualityMap(bandwidth: 32000000, width: 3840, height: 2160);
case RepositoryVideoQuality.p1440:
return _createQualityMap(bandwidth: 12000000, width: 2560, height: 1440);
case RepositoryVideoQuality.p1080:
return _createQualityMap(bandwidth: 8000000, width: 1920, height: 1080);
case RepositoryVideoQuality.p720:
return _createQualityMap(bandwidth: 3500000, width: 1280, height: 720);
case RepositoryVideoQuality.p480:
return _createQualityMap(bandwidth: 1800000, width: 854, height: 480);
case RepositoryVideoQuality.p360:
return _createQualityMap(bandwidth: 1200000, width: 640, height: 360);
case RepositoryVideoQuality.p240:
return _createQualityMap(bandwidth: 800000, width: 426, height: 240);
case RepositoryVideoQuality.p144:
return _createQualityMap(bandwidth: 400000, width: 256, height: 144);
default:
return _createQualityMap(bandwidth: 0, width: 0, height: 0);
}
}
static Map<String, int> _createQualityMap({required int bandwidth,
required int width,
required int height}) {
return {
'bandwidth': bandwidth,
'height': height,
'width': width,
};
}
}
@@ -8,25 +8,24 @@ class PlayerButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: 103,
child: CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 20),
borderRadius: const BorderRadius.all(Radius.circular(10)),
return Expanded(
child: CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 10),
borderRadius: const BorderRadius.all(Radius.circular(10)),
color: CupertinoColors.link,
child: Text(title,
textAlign: TextAlign.center,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.w500
)
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.white,
fontWeight: FontWeight.w500)
),
onPressed: () {
if (onPressed != null) {
onPressed!();
}
}
),
)
);
}
}
}
@@ -0,0 +1,139 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/domain/custom_skin_bloc/custom_skin_bloc.dart';
class PlayerCustomSkin extends StatelessWidget {
const PlayerCustomSkin({super.key});
@override
Widget build(BuildContext context) {
final bloc = CustomSkinBloc(context.read<PlayerViewBloc>());
return BlocProvider(
create: (context) => bloc,
child: GestureDetector(
onTap: () { bloc.add(SkinVisibilityChanged()); },
child: BlocBuilder<CustomSkinBloc, CustomSkinState>(
builder: (context, state) {
final skinState = state;
if (skinState is! CustomSkin) { return const Text('Incorrect widget'); }
return AbsorbPointer(
absorbing: !skinState.isVisible,
child: Visibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
visible: skinState.isVisible,
child: Container(
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 2,
colors: [
CupertinoColors.white.withOpacity(0.1),
CupertinoColors.systemMint.withOpacity(1)
]
)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoButton(
onPressed: () {
bloc.add(PlayerSeekBackEvent());
},
child: Image.asset('assets/skinIcons/rewind_back.png', width: 50)
),
_buildPlaybackButton(bloc),
CupertinoButton(
onPressed: () {
bloc.add(PlayerSeekForwardEvent());
},
child: Image.asset('assets/skinIcons/rewind_forward.png', width: 50)
),
]
),
if (skinState.duration >= skinState.currentPosition && skinState.duration > 0)
_buildTimeArea(bloc)
],
)),
),
);
},
),
),
);
}
Widget _buildTimeArea(CustomSkinBloc skinBloc) {
final skinState = skinBloc.state;
if (skinState is! CustomSkin) { return const Text('Unsupported view'); }
return Column(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: CupertinoSlider(
value: skinState.currentPosition,
activeColor: CupertinoColors.systemMint,
thumbColor: CupertinoColors.white,
min: 0,
max: skinState.duration,
onChangeEnd: (value) {
skinBloc.add(PlayerSeekEvent(value));
},
onChanged: (value) {
skinBloc.add(CurrentPositionChanged(value.roundToDouble()));
}),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(_timeFormatter(skinState.currentPosition),
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 12,
decoration: TextDecoration.none)),
const Spacer(),
Text(_timeFormatter(skinState.duration),
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 12,
decoration: TextDecoration.none)),
],
),
)
],
);
}
Widget _buildPlaybackButton(CustomSkinBloc skinBloc) {
final skinState = skinBloc.state;
if (skinState is! CustomSkin) { return const Text('Incorrect widget'); }
return CupertinoButton(
onPressed: () {
skinBloc.add(PlaybackChanged());
},
child: Image.asset(skinState.isPlaying ? 'assets/skinIcons/pause.png' : 'assets/skinIcons/play.png', width: 70)
);
}
String _timeFormatter(double time) {
Duration duration = Duration(seconds: time.round());
return [duration.inHours, duration.inMinutes, duration.inSeconds]
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
.join(':');
}
}
@@ -0,0 +1,134 @@
import 'package:flutter/cupertino.dart';
import '../../../common/models/option_data.dart';
class PlayerSettingsView extends StatelessWidget {
final List<OptionDataContainer> options;
final Function(OptionData, Key)? onChange;
const PlayerSettingsView({required this.options, this.onChange, super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(5))
),
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.only(top: 5, bottom: 30),
itemBuilder: (context, index) {
final item = options[index];
return Column(children: <Widget>[
_buildNavTile(item, context),
_buildSeparator()
]);
},
itemCount: options.length,
),
);
}
Widget _buildNavTile(OptionDataContainer option, BuildContext context) {
return CupertinoListTile(
title: Row(
children: [
Text(option.title, style: TextStyle(color: CupertinoColors.black.withOpacity(0.45), fontWeight: FontWeight.w400)),
const Spacer(),
Text(option.options[option.selectedIndex ?? 0].title, style: const TextStyle(color: CupertinoColors.black)),
const SizedBox(width: 7),
const Icon(CupertinoIcons.chevron_forward, color: CupertinoColors.systemGrey)
],
),
onTap: () async {
await Navigator.of(context).push<bool?>(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
pageBuilder: (_, __, ___) => _buildOptionsView(option, context)),
);
},
);
}
Widget _buildOptionsView(OptionDataContainer option, BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(5))
),
child: TapRegion(
onTapOutside: (_) {
Navigator.of(context)..pop()..pop();
},
child: ListView.builder(
padding: const EdgeInsets.only(top: 5, bottom: 30),
shrinkWrap: true,
itemBuilder: (context, index) {
if (index == 0) {
return Column(children: [
_buildBackButton(option, context),
_buildSeparator()
]);
} else {
return Column(children: [
_buildOptionTile(option, index - 1, context),
_buildSeparator()
]);
}
},
itemCount: option.options.length + 1,
),
),
),
);
}
Widget _buildBackButton(OptionDataContainer option, BuildContext context) {
return CupertinoListTile(
title: Row(
children: [
const Icon(CupertinoIcons.chevron_back, color: CupertinoColors.systemGrey),
const SizedBox(width: 5),
Text(option.title, style: TextStyle(color: CupertinoColors.black.withOpacity(0.45), fontWeight: FontWeight.w400)),
],
),
onTap: () {
Navigator.of(context).pop();
},
);
}
Widget _buildSeparator() {
return Container(
margin: const EdgeInsets.only(left: 20),
height: 0.8,
color: CupertinoColors.systemGrey4
);
}
Widget _buildOptionTile(OptionDataContainer option, int index, BuildContext context) {
final currentOption = option.options[index];
final isSelected = option.selectedIndex == index;
return CupertinoListTile(
title: Row(
children: [
if (isSelected) const Icon(CupertinoIcons.checkmark, color: CupertinoColors.black),
SizedBox(width: (isSelected) ? 6 : 30),
Text(currentOption.title, style: const TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w400)),
const Spacer()
],
),
onTap: () {
final key = option.key;
if (key != null) {
onChange?.call(currentOption, key);
Navigator.of(context)..pop()..pop();
}
},
);
}
}
@@ -1,173 +1,392 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/common/views/input_view.dart';
import 'package:nut_player_example/src/common/views/options_view.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_button.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_custom_skin.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_settings_view.dart';
class PlayerView extends StatelessWidget {
PlayerView({super.key});
class PlayerView extends StatefulWidget {
const PlayerView({super.key});
final List<OptionData> _playerAPIOptions = [
OptionData(key: const Key('PlaybackOptionVolumeID'), title: 'Громкость'),
OptionData(key: const Key('PlaybackOptionQualityID'), title: 'Качество'),
OptionData(key: const Key('PlaybackOptionSubsID'), title: 'Субтитры'),
OptionData(key: const Key('PlaybackOptionSpeedID'), title: 'Скорость')
];
final List<OptionData> _volumeOptions = [
OptionData(key: const Key('PlaybackOptionVolume00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionVolume01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionVolume02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionVolume03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionVolume04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionVolume05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionVolume06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionVolume07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionVolume08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionVolume09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionVolume10ID'), title: '1.0')
];
final List<OptionData> _speedOptions = [
OptionData(key: const Key('PlaybackOptionSpeed025xID'), title: '0.25x'),
OptionData(key: const Key('PlaybackOptionSpeed05xID'), title: '0.5x'),
OptionData(key: const Key('PlaybackOptionSpeed075xID'), title: '0.75x'),
OptionData(key: const Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', isSelected: true),
OptionData(key: const Key('PlaybackOptionSpeed125xID'), title: '1.25x'),
OptionData(key: const Key('PlaybackOptionSpeed15xID'), title: '1.5x'),
OptionData(key: const Key('PlaybackOptionSpeed175xID'), title: '1.75x'),
OptionData(key: const Key('PlaybackOptionSpeed2xID'), title: '2x'),
];
final List<OptionData> _qualityOptions = [
OptionData(key: const Key('PlaybackOptionQualityAutoID'), title: 'Авто', isSelected: true),
OptionData(key: const Key('PlaybackOptionQuality4kID'), title: '4K'),
OptionData(key: const Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD'),
OptionData(key: const Key('PlaybackOptionQuality1080pID'), title: '1080p FHD'),
OptionData(key: const Key('PlaybackOptionQuality720pID'), title: '720p HD'),
OptionData(key: const Key('PlaybackOptionQuality480pID'), title: '480p'),
OptionData(key: const Key('PlaybackOptionQuality360pID'), title: '360p'),
OptionData(key: const Key('PlaybackOptionQuality240pID'), title: '240p'),
OptionData(key: const Key('PlaybackOptionQuality144pID'), title: '144p')
];
final List<OptionData> _subsOptions = [
OptionData(key: const Key('PlaybackOptionSubsOffID'), title: 'Выкл.', isSelected: true),
OptionData(key: const Key('PlaybackOptionSubsAutoID'), title: 'Созданы автоматически'),
OptionData(key: const Key('PlaybackOptionSubsEngID'), title: 'Английские'),
OptionData(key: const Key('PlaybackOptionSubsRusID'), title: 'Русские')
];
final NumericOptionData _startPositionOption = NumericOptionData(key: const Key('PlaybackOptionStartPositionID'), title: 'Стартовая позиция', value: 0);
@override
State<PlayerView> createState() => _PlayerViewState();
}
class _PlayerViewState extends State<PlayerView> {
final TextEditingController _textController = TextEditingController();
StreamSubscription<String>? _logSubscription;
StreamSubscription<Object>? _skinActionsSubscription;
late PlayerViewBloc _bloc;
var _turns = 0;
bool _isFullScreen = false;
@override
void initState() {
super.initState();
_bloc = context.read<PlayerViewBloc>();
_logSubscription = _bloc.controller.logStream.listen((event) {
final text = _textController.text;
_textController.text = '$event\r\n$text';
});
_skinActionsSubscription = _bloc.controller.skinActionStream.listen((event) {
final call = event as MethodCall;
switch (call.method) {
case 'pluginSkinDefaultAction':
final fullscreen = call.arguments["onEnter"];
final repository = context.read<SettingsRepository>();
if (_isFullScreen != fullscreen &&
repository.fullscreenSettings != RepositoryFullscreenSettings.off) {
setState(() {
_isFullScreen = fullscreen;
if (fullscreen) {
// В случае ландшафтного режима нам нужно сделать 3 поворота,
// чтобы сделать ориентацию интерфейса как в ютубе.
// Можно повернуть на 1, но тогда интерфейс не будет по тому же
// краю, как в иосном демо-приложении
_turns = repository.fullscreenSettings == RepositoryFullscreenSettings.landscape ? 3 : 0;
} else {
_turns = 0;
}
});
}
case 'pluginSkinSettingsAction':
showCupertinoModalPopup(
context: context,
builder: (_) {
final speedSettings = PlayerViewController.playbackSettings.firstWhere((element) => element.key == const Key('PlaybackSpeedSettingID'));
final updatedSpeedSetting = _updatedSetting(speedSettings);
var options = [updatedSpeedSetting];
final qualitiesSettings = _createQualities(_bloc);
if (qualitiesSettings != null) {
final updatedQualitiesSettings = _updatedSetting(qualitiesSettings);
options.add(updatedQualitiesSettings);
}
final subsSettings = _createSubs(_bloc);
if (subsSettings != null) {
final updatedSubsSettings = _updatedSetting(subsSettings);
options.add(updatedSubsSettings);
}
return PlayerSettingsView(
options: options,
onChange: (option, key) {
switch (key) {
case const Key('PlaybackSpeedSettingID'):
_bloc.add(SpeedChangedEvent(option));
break;
case const Key('PlaybackSubsSettingID'):
_bloc.add(SubsChangedEvent(option));
break;
case const Key('PlaybackQualitySettingID'):
_bloc.add(QualityChangedEvent(option));
break;
default:
break;
}
});
}
);
}
});
}
@override
Widget build(BuildContext context) {
_textController.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
final videoPlayer = VideoPlayer(_bloc.controller);
return _isFullScreen ? _makeFullscreenWidget(videoPlayer)
: _makePortraitWidget(videoPlayer);
}
@override
void deactivate() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
_logSubscription?.cancel();
_logSubscription = null;
_skinActionsSubscription?.cancel();
_skinActionsSubscription = null;
final bloc = context.read<PlayerViewBloc>();
bloc.controller.dispose();
super.deactivate();
}
Widget _makePortraitWidget(VideoPlayer videoPlayer) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: const CupertinoNavigationBar(
key: Key('PlayerAppBarID'),
middle: Text('Демо NutPlayer',
style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)
),
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
padding: EdgeInsetsDirectional.all(0),
navigationBar: CupertinoNavigationBar(
key: const Key('PlayerAppBarID'),
middle: const Text('Демо NutPlayer',
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.black,
fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(
previousPageTitle: 'Назад',
onPressed: () {
_bloc.add(const DismissEvent());
}),
padding: const EdgeInsetsDirectional.all(0),
backgroundColor: CupertinoColors.white,
),
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
AspectRatio(aspectRatio: 16/10,
// TODO: Встроить плеер
child: Container(
color: CupertinoColors.link,
)
),
child: BlocBuilder<PlayerViewBloc, PlayerViewState>(
builder: (context, state) {
return Column(children: [
AspectRatio(
aspectRatio: 16 / 10,
child: _buildPlayer(videoPlayer, context),
),
const SizedBox(height: 10),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PlayerButton('Пуск', () {
_bloc.add(const PlayEvent());
}),
const SizedBox(width: 10),
PlayerButton('Пауза', () {
_bloc.add(const PauseEvent());
}),
const SizedBox(width: 10),
PlayerButton('Завершить', () {
_bloc.add(const EndEvent());
})
],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PlayerButton('Пуск', () {}),
PlayerButton('Пауза', () {}),
PlayerButton('Стоп', () {})
],
),
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('API Проигрывателя'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
Expanded(
child: ListView(
shrinkWrap: true,
children: [
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('API Проигрывателя'.toUpperCase(),
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400)),
),
),
children: <Widget>[..._playerAPIOptions.map((value) {
if (value.key == const Key('PlaybackOptionVolumeID')) {
return OptionsView(value.title, _volumeOptions);
} else if (value.key == const Key('PlaybackOptionSpeedID')) {
return OptionsView(value.title, _speedOptions);
} else if (value.key == const Key('PlaybackOptionQualityID')) {
return OptionsView(value.title, _qualityOptions);
} else {
return OptionsView(value.title, _subsOptions);
}
}).toList()]
),
children: _buildDynamicWidgets(PlayerViewController.playbackSettings, _bloc)
),
CupertinoListSection.insetGrouped(
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
hasLeading: false,
children: _buildWidgets([PlayerViewController.startPosition], _bloc)
),
CupertinoListSection.insetGrouped(
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
hasLeading: false,
children: <Widget>[InputView(_startPositionOption)]
),
if (context.read<SettingsRepository>().log != RepositoryLogType.off)
_buildLog()
]
)
)
]);
},
),
));
}
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Лог плеера'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
const Spacer(),
SizedBox(
height: 25,
child: CupertinoButton(
onPressed: () { _textController.text = ''; },
padding: const EdgeInsets.all(0),
child: const Text('Очистить')),
)
],
),
),
children: <Widget>[
CupertinoTextField(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 18),
decoration: const BoxDecoration(
border: null,
borderRadius: BorderRadius.all(Radius.circular(12)),
color: CupertinoColors.white,
),
minLines: 1,
maxLines: 10,
readOnly: true,
maxLength: null,
autocorrect: false,
keyboardType: TextInputType.multiline,
controller: _textController,
),
]
),
]
),
)
Widget _buildLog() {
return CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Лог плеера'.toUpperCase(),
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400)),
const Spacer(),
SizedBox(
height: 25,
child: CupertinoButton(
onPressed: () {
_textController.text = '';
},
padding: const EdgeInsets.all(0),
child: const Text('Очистить')),
)
],
),
),
children: <Widget>[
CupertinoTextField(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 18),
decoration: const BoxDecoration(
border: null,
borderRadius: BorderRadius.all(Radius.circular(12)),
color: CupertinoColors.white,
),
minLines: 1,
maxLines: 10,
readOnly: true,
maxLength: null,
autocorrect: false,
keyboardType: TextInputType.multiline,
controller: _textController,
),
]
);
}
List<Widget> _buildDynamicWidgets(List<Object> objects, PlayerViewBloc bloc) {
var widgets = _buildWidgets(objects, bloc);
final qualities = _createQualities(bloc);
if (qualities != null) {
final qualityWidget = _buildOptionsView(qualities, bloc);
widgets.insert(1, qualityWidget);
}
final subtitles = _createSubs(bloc);
if (subtitles != null) {
final subtitleWidget = _buildOptionsView(subtitles, bloc);
widgets.insert(2, subtitleWidget);
}
return widgets;
}
OptionDataContainer? _createQualities(PlayerViewBloc bloc) {
final state = bloc.state;
if (state is! PlayerViewController) { return null; }
return PlayerViewController.createQualities(state.qualities);
}
OptionDataContainer? _createSubs(PlayerViewBloc bloc) {
final state = bloc.state;
if (state is! PlayerViewController) { return null; }
return PlayerViewController.createSubs(state.subtitles);
}
OptionDataContainer _updatedSetting(OptionDataContainer oldValue) {
return OptionDataContainer(
key: oldValue.key,
title: oldValue.title,
options: oldValue.options,
selectedIndex: _bloc.selectedIndex(oldValue),
onSelectedOption: oldValue.onSelectedOption
);
}
Widget _buildOptionsView(OptionDataContainer setting, PlayerViewBloc bloc) {
final selectedIndex = bloc.selectedIndex(setting);
return OptionsView(
setting.title,
setting.options,
selectedIndex,
(selectedIndex) {
final selectedOption = setting.options[selectedIndex];
final event = setting.onSelectedOption?.call(selectedOption);
if (event != null) {
bloc.add(event);
}
},
key: setting.key
);
}
List<Widget> _buildWidgets(List<Object> objects, PlayerViewBloc bloc) {
return <Widget>[...objects.map((setting) {
if (setting is OptionDataContainer) {
return _buildOptionsView(setting, bloc);
} else if (setting is NumericOptionData) {
final currentValue = bloc.currentNumericValue(setting);
return InputView(
setting.title,
currentValue, (newTime) {
final event = setting.onChange?.call(newTime);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else {
return const Text('not implemented');
}
}).toList()];
}
Widget _buildPlayer(VideoPlayer videoPlayer, BuildContext context) {
List<Widget> widgets = [videoPlayer];
final repository = context.read<SettingsRepository>();
final isSkinByDefault = repository.isSkinByDefault;
if (!isSkinByDefault) {
widgets.add(_buildCustomSkin());
}
return isSkinByDefault ? videoPlayer : Stack(children: widgets);
}
Widget _buildCustomSkin() {
return const PlayerCustomSkin();
}
Widget _makeFullscreenWidget(VideoPlayer videoPlayer) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
final size = MediaQuery.of(context).size;
return SafeArea(
child:
OrientationBuilder(
builder: (context, orientation) {
final repository = context.read<SettingsRepository>();
switch (orientation) {
case Orientation.portrait:
// в случае режима .landscape нужно поворачивать на 90 градусов 2 раза,
// т.к. 1 раз поворачивается сам экран + надо поворачивать
// представление, чтобы проскочить свободное положение экрана.
// Для .flexible не требуются дополнительные повороты, видео вращается вместе с экраном.
_turns += repository.fullscreenSettings == RepositoryFullscreenSettings.flexible ? 0 : 2;
case Orientation.landscape:
// в случае ландшатного режима нам нужно компенсировать поворот
// самого экрана, поэтому отнимаем один поворот на 90 градусов
_turns += repository.fullscreenSettings == RepositoryFullscreenSettings.flexible ? 0 : -1;
}
return RotatedBox(
quarterTurns: _turns,
child: Container(
width: size.width,
height: size.height,
color: CupertinoColors.black,
child: videoPlayer,
)
);
}
)
);
}
@@ -0,0 +1,330 @@
import 'dart:async';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/player_screen/mapper/settings_mapper.dart';
part 'settings_event.dart';
part 'settings_state.dart';
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final SettingsRepository _repository;
StreamSubscription<String>? _versionSubscription;
SettingsBloc(this._repository) : super(SettingsInitialState.createFromRepository(_repository)) {
_onInitialize();
on<DismissEvent>(_onDismissEvent);
on<CrashEvent>(_onCrashEvent);
on<SkinChangedEvent>(_onSkinChangedEvent);
on<SpeedChangedEvent>(_onSpeedChangedEvent);
on<VolumeChangedEvent>(_onVolumeChangedEvent);
on<BrightnessChangedEvent>(_onBrightnessChangedEvent);
on<AutostartChangedEvent>(_onAutostartChangedEvent);
on<FullscreenChangedEvent>(_onFullscreenChangedEvent);
on<PipChangedEvent>(_onPipChangedEvent);
on<SettingsChangedEvent>(_onSettingsChangedEvent);
on<ColorChangedEvent>(_onColorChangedEvent);
on<QualityChangedEvent>(_onQualityChangedEvent);
on<QualityNamingChangedEvent>(_onQualityNamingChangedEvent);
on<SubsChangedEvent>(_onSubsChangedEvent);
on<StartPositionChangedEvent>(_onStartPositionChangedEvent);
on<LoopChangedEvent>(_onLoopChangedEvent);
on<PlaylistTimeoutsChangedEvent>(_onPlaylistTimeoutsChangedEvent);
on<TrackTimeoutsChangedEvent>(_onTrackTimeoutsChangedEvent);
on<ChunkTimeoutsChangedEvent>(_onChunkTimeoutsChangedEvent);
on<LogTypeChangedEvent>(_onLogTypeChangedEvent);
on<VersionReceivedEvent>(_onVersionReceived);
}
_onInitialize() {
_versionSubscription = PlayerVersionObserver().versionStream.listen((version) {
add(VersionReceivedEvent(version));
});
}
_onVersionReceived(VersionReceivedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
emit(settingsState.copy(playerVersion: event.version));
}
_onDismissEvent(DismissEvent event, Emitter<SettingsState> emit) {
_versionSubscription?.cancel();
_versionSubscription = null;
emit(SettingsDismiss());
}
_onCrashEvent(CrashEvent event, Emitter<SettingsState> emit) {
FirebaseCrashlytics.instance.crash();
}
_onSkinChangedEvent(SkinChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSkinByDefault = event.isSkinByDefault;
emit(settingsState.copy(isSkinByDefault: event.isSkinByDefault));
}
_onSpeedChangedEvent(SpeedChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final speedWithoutX = event.option.title.replaceAll(RegExp('x'), '');
final speed = double.tryParse(speedWithoutX) ?? 1;
_repository.speed = speed;
emit(settingsState.copy(speed: speed));
}
_onFullscreenChangedEvent(FullscreenChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final fullscreenMode = FullscreenSettings.values.firstWhere((element) => element.title == event.option.value);
_repository.fullscreenSettings = SettingsMapper.repoFullscreenSettings(fullscreenMode);
emit(settingsState.copy(fullscreenSettings: fullscreenMode));
}
_onVolumeChangedEvent(VolumeChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final volume = double.parse(event.option.title);
_repository.volume = volume;
emit(settingsState.copy(volume: volume));
}
_onBrightnessChangedEvent(BrightnessChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final brightness = double.parse(event.option.title);
_repository.brightness = brightness;
emit(settingsState.copy(brightness: brightness));
}
_onStartPositionChangedEvent(StartPositionChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.start = event.value;
emit(settingsState.copy(start: event.value));
}
_onPlaylistTimeoutsChangedEvent(PlaylistTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.playlist = event.playlist;
emit(settingsState.copy(playlist: event.playlist));
}
_onTrackTimeoutsChangedEvent(TrackTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.track = event.track;
emit(settingsState.copy(track: event.track));
}
_onChunkTimeoutsChangedEvent(ChunkTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.chunk = event.chunk;
emit(settingsState.copy(chunk: event.chunk));
}
_onAutostartChangedEvent(AutostartChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isAutostart = event.isOn;
emit(settingsState.copy(isAutostart: event.isOn));
}
_onLoopChangedEvent(LoopChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isLoop = event.isOn;
emit(settingsState.copy(isLoop: event.isOn));
}
_onPipChangedEvent(PipChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isPipAvailable = event.isOn;
emit(settingsState.copy(isPipAvailable: event.isOn));
}
_onSettingsChangedEvent(SettingsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSettingsAvailable = event.isOn;
emit(settingsState.copy(isSettingsAvailable: event.isOn));
}
_onSubsChangedEvent(SubsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSubtitlesAvailable = event.isOn;
emit(settingsState.copy(isSubtitlesAvailable: event.isOn));
}
_onLogTypeChangedEvent(LogTypeChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
switch (event.log) {
case LogType.info:
_repository.log = RepositoryLogType.info;
case LogType.debug:
_repository.log = RepositoryLogType.debug;
case LogType.off:
_repository.log = RepositoryLogType.off;
}
emit(settingsState.copy(log: event.log));
}
_onQualityChangedEvent(QualityChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final quality = event.option.value;
if (quality == null) { return; }
final repoQuality = SettingsMapper.repoVideoQuality(quality);
_repository.quality = repoQuality;
final mappedQuality = SettingsMapper.quality(repoQuality);
emit(settingsState.copy(quality: mappedQuality));
}
_onQualityNamingChangedEvent(QualityNamingChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final qualityNamingString = event.option.value;
if (qualityNamingString == null) { return; }
final repoQualityNaming = SettingsMapper.repoQualityNaming(qualityNamingString);
_repository.qualityNaming = repoQualityNaming;
emit(settingsState.copy(
qualityNaming: SettingsMapper.qualityNaming(repoQualityNaming))
);
}
_onColorChangedEvent(ColorChangedEvent event, Emitter<SettingsState> emit) {}
int currentNumericValue(NumericOptionData setting) {
if (setting.key == const Key('PlaybackOptionStartPositionID')) {
return _repository.start;
} else if (setting.key == const Key('TimeoutsOptionManifestID')) {
return _repository.playlist;
} else if (setting.key == const Key('TimeoutsOptionTrackID')) {
return _repository.track;
} else if (setting.key == const Key('TimeoutsOptionChunkID')) {
return _repository.chunk;
} else {
return 0;
}
}
int selectedIndex(OptionDataContainer setting) {
if (setting.key == const Key('PlaybackOptionSpeedID')) {
return _selectedIndexForSpeed(setting.options);
} else if (setting.key == const Key('PlaybackOptionVolumeID')) {
return _selectedIndexForVolume(setting.options);
} else if (setting.key == const Key('PlaybackOptionBrightnessID')) {
return _selectedIndexForBrightness(setting.options);
} else if (setting.key == const Key('FullscreenID')) {
return _selectedIndexForFullscreen(setting.options);
} else if (setting.key == const Key('PlaybackOptionQualityID')) {
return _selectedIndexForQuality(setting.options);
} else if (setting.key == const Key('PlaybackOptionQualityNamingID')) {
return _selectedIndexForQualityNaming(setting.options);
}
else {
return 0;
}
}
bool isSelected(BoolOptionData setting) {
if (setting.key == const Key('PlaybackOptionAutostartID')) {
return _repository.isAutostart;
} else if (setting.key == const Key('ExtraOptionLoopID')) {
return _repository.isLoop;
} else if (setting.key == const Key('SkinStandardPipID')) {
return _repository.isPipAvailable;
} else if (setting.key == const Key('SkinStandardSettingsID')) {
return _repository.isSettingsAvailable;
} else if (setting.key == const Key('SkinOptionDefaultID')) {
return _repository.isSkinByDefault;
} else if (setting.key == const Key('SkinOptionByUserID')) {
return !_repository.isSkinByDefault;
} else if (setting.key == const Key('PlaybackOptionSubtitlesID')) {
return _repository.isSubtitlesAvailable;
} else {
return false;
}
}
int _selectedIndexForSpeed(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 3; }
return settings.indexWhere((element) => element.value == settingsState.speed.toString());
}
int _selectedIndexForVolume(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 5; }
return settings.indexWhere((element) => element.value == settingsState.volume.toString());
}
int _selectedIndexForBrightness(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 5; }
return settings.indexWhere((element) {
double rounded = (settingsState.brightness*10).roundToDouble();
double bright = rounded/10.0;
return bright.toString() == element.value;
});
}
int _selectedIndexForFullscreen(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.fullscreenSettings.title);
}
int _selectedIndexForQuality(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.quality.identifier);
}
int _selectedIndexForQualityNaming(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.qualityNaming.name);
}
}
@@ -0,0 +1,107 @@
part of 'settings_bloc.dart';
@immutable
abstract class SettingsEvent {}
class DismissEvent extends SettingsEvent {}
class CrashEvent extends SettingsEvent {}
class VersionReceivedEvent extends SettingsEvent {
final String version;
VersionReceivedEvent(this.version);
}
class SpeedChangedEvent extends SettingsEvent {
final OptionData option;
SpeedChangedEvent(this.option);
}
class VolumeChangedEvent extends SettingsEvent {
final OptionData option;
VolumeChangedEvent(this.option);
}
class SkinChangedEvent extends SettingsEvent {
final bool isSkinByDefault;
SkinChangedEvent(this.isSkinByDefault);
}
class BrightnessChangedEvent extends SettingsEvent {
final OptionData option;
BrightnessChangedEvent(this.option);
}
class QualityChangedEvent extends SettingsEvent {
final OptionData option;
QualityChangedEvent(this.option);
}
class QualityNamingChangedEvent extends SettingsEvent {
final OptionData option;
QualityNamingChangedEvent(this.option);
}
class FullscreenChangedEvent extends SettingsEvent {
final OptionData option;
FullscreenChangedEvent(this.option);
}
class ColorChangedEvent extends SettingsEvent {
final OptionData option;
ColorChangedEvent(this.option);
}
class SubsChangedEvent extends SettingsEvent {
final bool isOn;
SubsChangedEvent(this.isOn);
}
class PipChangedEvent extends SettingsEvent {
final bool isOn;
PipChangedEvent(this.isOn);
}
class SettingsChangedEvent extends SettingsEvent {
final bool isOn;
SettingsChangedEvent(this.isOn);
}
class AutostartChangedEvent extends SettingsEvent {
final bool isOn;
AutostartChangedEvent(this.isOn);
}
class LoopChangedEvent extends SettingsEvent {
final bool isOn;
LoopChangedEvent(this.isOn);
}
class StartPositionChangedEvent extends SettingsEvent {
final int value;
StartPositionChangedEvent(this.value);
}
class PlaylistTimeoutsChangedEvent extends SettingsEvent {
final int playlist;
PlaylistTimeoutsChangedEvent(this.playlist);
}
class TrackTimeoutsChangedEvent extends SettingsEvent {
final int track;
TrackTimeoutsChangedEvent(this.track);
}
class ChunkTimeoutsChangedEvent extends SettingsEvent {
final int chunk;
ChunkTimeoutsChangedEvent(this.chunk);
}
class LogTypeChangedEvent extends SettingsEvent {
final LogType log;
LogTypeChangedEvent(this.log);
}
@@ -0,0 +1,360 @@
part of 'settings_bloc.dart';
enum FullscreenSettings { landscape, flexible, off }
extension FullscreenSettingsExtension on FullscreenSettings {
String get title {
switch (this) {
case FullscreenSettings.landscape:
return 'Альбомный';
case FullscreenSettings.flexible:
return 'Свободный';
case FullscreenSettings.off:
return 'Выкл.';
default:
return 'Неизвестно';
}
}
}
enum SkinColor { white }
enum VideoQualityNaming { common, rus, eng, resolution }
enum LogType { off, info, debug }
enum VideoQuality { auto, p2160, p1440, p1080, p720, p480, p360, p240, p144 }
extension VideoQualityIdentifier on VideoQuality {
String get identifier {
switch (this) {
case VideoQuality.auto:
return "auto";
case VideoQuality.p144:
return "p144";
case VideoQuality.p240:
return "p240";
case VideoQuality.p360:
return "p360";
case VideoQuality.p480:
return "p480";
case VideoQuality.p720:
return "p720";
case VideoQuality.p1080:
return "p1080";
case VideoQuality.p1440:
return "p1440";
case VideoQuality.p2160:
return "p2160";
}
}
}
sealed class SettingsState {}
class SettingsDismiss extends SettingsState {}
@immutable
class SettingsInitialState extends SettingsState {
final String playerVersion;
final bool isSkinByDefault;
final FullscreenSettings fullscreenSettings;
final bool isPipAvailable;
final bool isSettingsAvailable;
final SkinColor skinColor;
final bool isAutostart;
final double volume;
final double brightness;
final double speed;
final VideoQualityNaming qualityNaming;
final VideoQuality quality;
final bool isSubtitlesAvailable;
final int start;
final bool isLoop;
final int playlist;
final int track;
final int chunk;
final LogType log;
SettingsInitialState({
required this.playerVersion,
required this.isSkinByDefault,
required this.fullscreenSettings,
required this.isPipAvailable,
required this.isSettingsAvailable,
required this.skinColor,
required this.isAutostart,
required this.brightness,
required this.volume,
required this.speed,
required this.qualityNaming,
required this.quality,
required this.isSubtitlesAvailable,
required this.start,
required this.isLoop,
required this.playlist,
required this.track,
required this.chunk,
required this.log
});
SettingsInitialState copy({
String? playerVersion,
bool? isSkinByDefault,
FullscreenSettings? fullscreenSettings,
bool? isPipAvailable,
bool? isSettingsAvailable,
SkinColor? skinColor,
bool? isAutostart,
double? volume,
double? brightness,
double? speed,
VideoQualityNaming? qualityNaming,
VideoQuality? quality,
bool? isSubtitlesAvailable,
int? start,
bool? isLoop,
int? playlist,
int? track,
int? chunk,
LogType? log
}) {
return SettingsInitialState(
playerVersion: playerVersion ?? this.playerVersion,
isSkinByDefault: isSkinByDefault ?? this.isSkinByDefault,
fullscreenSettings: fullscreenSettings ?? this.fullscreenSettings,
isPipAvailable: isPipAvailable ?? this.isPipAvailable,
isSettingsAvailable: isSettingsAvailable ?? this.isSettingsAvailable,
skinColor: skinColor ?? this.skinColor,
isAutostart: isAutostart ?? this.isAutostart,
volume: volume ?? this.volume,
brightness: brightness ?? this.brightness,
speed: speed ?? this.speed,
qualityNaming: qualityNaming ?? this.qualityNaming,
quality: quality ?? this.quality,
isSubtitlesAvailable: isSubtitlesAvailable ?? this.isSubtitlesAvailable,
start: start ?? this.start,
isLoop: isLoop ?? this.isLoop,
playlist: playlist ?? this.playlist,
track: track ?? this.track,
chunk: chunk ?? this.chunk,
log: log ?? this.log
);
}
factory SettingsInitialState.createFromRepository(SettingsRepository repository) {
return SettingsInitialState(
playerVersion: "",
isSkinByDefault: repository.isSkinByDefault,
fullscreenSettings: SettingsMapper.fullscreenSetting(repository.fullscreenSettings),
isPipAvailable: repository.isPipAvailable,
isSettingsAvailable: repository.isSettingsAvailable,
skinColor: SettingsMapper.skinColor(repository.skinColor),
isAutostart: repository.isAutostart,
volume: repository.volume,
brightness: repository.brightness,
speed: repository.speed,
qualityNaming: SettingsMapper.qualityNaming(repository.qualityNaming),
quality: SettingsMapper.quality(repository.quality),
isSubtitlesAvailable: repository.isSubtitlesAvailable,
start: repository.start,
isLoop: repository.isLoop,
playlist: repository.playlist,
track: repository.track,
chunk: repository.chunk,
log: SettingsMapper.logType(repository.log)
);
}
static final skinSettings = [
BoolOptionData(
key: const Key('SkinOptionDefaultID'),
title: 'По умолчанию',
isSelected: true,
onChange: (_) => SkinChangedEvent(true)
),
BoolOptionData(
key: const Key('SkinOptionByUserID'),
title: 'Свой',
isSelected: false,
onChange: (_) => SkinChangedEvent(false)
)
];
static final standardSkinSettings = [
OptionDataContainer(
key: const Key('FullscreenID'),
title: 'Полноэкранный режим',
options: const [
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenAlbumID'), 'Альбомный'),
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenFlexID'),'Свободный'),
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenOffID'), "Выкл.")
],
onSelectedOption: (option) => FullscreenChangedEvent(option)
),
BoolOptionData(
key: const Key('SkinStandardPipID'),
title: 'Картинка в картинке',
isSelected: false,
onChange: (isOn) => PipChangedEvent(isOn)
),
BoolOptionData(
key: const Key('SkinStandardSettingsID'),
title: 'Настройки',
isSelected: true,
onChange: (isOn) => SettingsChangedEvent(isOn)
),
OptionDataContainer(
key: const Key('ColorID'),
title: 'Цвет',
options: const [
OptionData(key: Key('SkinStandardColor1ID'), title: '#0D70D1'),
OptionData(key: Key('SkinStandardColor2ID'), title: '#000000'),
OptionData(key: Key('SkinStandardColor3ID'), title: '#FFFFFF')
]
)
];
static final playbackOptions = [
BoolOptionData(
key: const Key('PlaybackOptionAutostartID'),
title: 'Автостарт',
isSelected: false,
onChange: (isOn) => AutostartChangedEvent(isOn)
),
OptionDataContainer(
key: const Key('PlaybackOptionVolumeID'),
title: 'Громкость',
options: const [
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume00ID'), '0.0'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume01ID'), '0.1'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume02ID'), '0.2'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume03ID'), '0.3'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume04ID'), '0.4'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume05ID'), '0.5'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume06ID'), '0.6'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume07ID'), '0.7'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume08ID'), '0.8'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume10ID'), '1.0')
],
onSelectedOption: (option) => VolumeChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionBrightnessID'),
title: 'Яркость',
options: const [
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness00ID'), '0.0'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness01ID'), '0.1'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness02ID'), '0.2'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness03ID'), '0.3'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness04ID'), '0.4'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness05ID'), '0.5'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness06ID'), '0.6'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness07ID'), '0.7'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness08ID'), '0.8'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness10ID'), '1.0')
],
onSelectedOption: (option) => BrightnessChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionSpeedID'),
title: 'Скорость',
options: const [
OptionData(key: Key('PlaybackOptionSpeed025xID'), title: '0.25x', value: '0.25'),
OptionData(key: Key('PlaybackOptionSpeed05xID'), title: '0.5x', value: '0.5'),
OptionData(key: Key('PlaybackOptionSpeed075xID'), title: '0.75x', value: '0.75'),
OptionData(key: Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', value: '1.0'),
OptionData(key: Key('PlaybackOptionSpeed125xID'), title: '1.25x', value: '1.25'),
OptionData(key: Key('PlaybackOptionSpeed15xID'), title: '1.5x', value: '1.5'),
OptionData(key: Key('PlaybackOptionSpeed175xID'), title: '1.75x', value: '1.75'),
OptionData(key: Key('PlaybackOptionSpeed2xID'), title: '2x', value: '2.0')
],
onSelectedOption: (option) => SpeedChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionQualityID'),
title: 'Качество',
options: const [
OptionData(key: Key('PlaybackOptionQualityAutoID'), title: 'Авто', value: "auto"),
OptionData(key: Key('PlaybackOptionQuality4kID'), title: '4K', value: "p2160"),
OptionData(key: Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD', value: "p1440"),
OptionData(key: Key('PlaybackOptionQuality1080pID'), title: '1080p FHD', value: "p1080"),
OptionData(key: Key('PlaybackOptionQuality720pID'), title: '720p HD', value: "p720"),
OptionData(key: Key('PlaybackOptionQuality480pID'), title: '480p', value: "p480"),
OptionData(key: Key('PlaybackOptionQuality360pID'), title: '360p', value: "p360"),
OptionData(key: Key('PlaybackOptionQuality240pID'), title: '240p', value: "p240"),
OptionData(key: Key('PlaybackOptionQuality144pID'), title: '144p', value: "p144"),
],
onSelectedOption: (option) => QualityChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionQualityNamingID'),
title: 'Название качества',
options: const [
OptionData(key: Key('PlaybackOptionQualityNameCommonID'), title: 'Common', value: "common"),
OptionData(key: Key('PlaybackOptionQualityNameRusID'), title: 'Russian', value: "rus"),
OptionData(key: Key('PlaybackOptionQualityNameEngID'), title: 'English', value: "eng"),
OptionData(key: Key('PlaybackOptionQualityNameResolutionID'), title: 'Resolution', value: "resolution"),
],
onSelectedOption: (option) => QualityNamingChangedEvent(option)
),
BoolOptionData(
key: const Key('PlaybackOptionSubtitlesID'),
title: 'Субтитры',
isSelected: true,
onChange: (isOn) => SubsChangedEvent(isOn)
),
NumericOptionData(
key: const Key('PlaybackOptionStartPositionID'),
title: 'Стартовая позиция',
value: 0,
onChange: (value) => StartPositionChangedEvent(value)
)
];
static final extraOption = BoolOptionData(
key: const Key('ExtraOptionLoopID'),
title: 'Зацикленность',
isSelected: false,
onChange: (isOn) => LoopChangedEvent(isOn)
);
static List<NumericOptionData> timeoutsOptions = [
NumericOptionData(
key: const Key('TimeoutsOptionManifestID'),
title: 'Манифест (мс)',
value: 5000,
onChange: (value) => PlaylistTimeoutsChangedEvent(value)
),
NumericOptionData(
key: const Key('TimeoutsOptionTrackID'),
title: 'Трек (мс)',
value: 3000,
onChange: (value) => TrackTimeoutsChangedEvent(value)
),
NumericOptionData(
key: const Key('TimeoutsOptionChunkID'),
title: 'Сегмент (мс)',
value: 3000,
onChange: (value) => ChunkTimeoutsChangedEvent(value)
)
];
static const logOptions = {
"LogType.off": OptionData(key: Key('LogOptionOffId'), title: 'Выкл.'),
"LogType.info": OptionData(key: Key('LogOptionInfoID'), title: 'Инфо.'),
"LogType.debug": OptionData(key: Key('LogOptionDebugID'), title: 'Отладка')
};
}
@@ -1,138 +1,40 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/views/checkmark_list_option.dart';
import 'package:nut_player_example/src/common/views/input_view.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
import 'package:nut_player_example/src/common/views/options_view.dart';
import 'package:nut_player_example/src/features/settings_screen/presentation/toggle_view.dart';
import 'package:package_info_plus/package_info_plus.dart';
enum LogType { off, info, debug }
class SettingsView extends StatelessWidget {
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
final List<OptionData> _skinOptions = [
OptionData(key: const Key('SkinOptionDefaultID'), title: 'По умолчанию', isSelected: true),
OptionData(key: const Key('SkinOptionByUserID'), title: 'Свой')
];
final List<OptionData> _standardSkinOptions = [
OptionData(key: const Key('SkinStandardOptionFullscreenID'), title: 'Полноэкранный режим'),
OptionData(key: const Key('SkinStandardOptionPipID'), title: 'Картинка в картинке', isSelected: true),
OptionData(key: const Key('SkinStandardOptionSettingsID'), title: 'Настройки', isSelected: true),
OptionData(key: const Key('SkinStandardOptionColorID'), title: 'Цвет')
];
final List<OptionData> _fullscreen = [
OptionData(key: const Key('SkinStandardFullscreenAlbumID'), title: 'Альбомный', isSelected: true),
OptionData(key: const Key('SkinStandardFullscreenFlexID'), title: 'Гибкий'),
OptionData(key: const Key('SkinStandardFullscreenOffID'), title: 'Выкл.')
];
final List<OptionData> _colors = [
OptionData(key: const Key('SkinStandardColor1ID'), title: '#0D70D1', isSelected: true),
OptionData(key: const Key('SkinStandardColor2ID'), title: '#000000'),
OptionData(key: const Key('SkinStandardColor3ID'), title: '#FFFFFF')
];
final List<OptionData> _playbackOptions = [
OptionData(key: const Key('PlaybackOptionAutostartID'), title: 'Автостарт'),
OptionData(key: const Key('PlaybackOptionVolumeID'), title: 'Громкость'),
OptionData(key: const Key('PlaybackOptionBrightnessID'), title: 'Яркость'),
OptionData(key: const Key('PlaybackOptionSpeedID'), title: 'Скорость'),
OptionData(key: const Key('PlaybackOptionQualityNamingID'), title: 'Название качества'),
OptionData(key: const Key('PlaybackOptionQualityID'), title: 'Качество'),
OptionData(key: const Key('PlaybackOptionSubtitlesID'), title: 'Субтитры', isSelected: true)
];
final NumericOptionData _startPositionOption = NumericOptionData(key: const Key('PlaybackOptionStartPositionID'), title: 'Стартовая позиция', value: 0);
final OptionData _extraOption = OptionData(key: const Key('ExtraOptionLoopID'), title: 'Зацикленность');
LogType _currentLogType = LogType.info;
final List<OptionData> _volumeOptions = [
OptionData(key: const Key('PlaybackOptionVolume00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionVolume01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionVolume02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionVolume03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionVolume04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionVolume05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionVolume06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionVolume07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionVolume08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionVolume09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionVolume10ID'), title: '1.0')
];
final List<OptionData> _brightnessOptions = [
OptionData(key: const Key('PlaybackOptionBrightness00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionBrightness01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionBrightness02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionBrightness03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionBrightness04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionBrightness05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionBrightness06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionBrightness07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionBrightness08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionBrightness09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionBrightness10ID'), title: '1.0')
];
final List<OptionData> _speedOptions = [
OptionData(key: const Key('PlaybackOptionSpeed025xID'), title: '0.25x'),
OptionData(key: const Key('PlaybackOptionSpeed05xID'), title: '0.5x'),
OptionData(key: const Key('PlaybackOptionSpeed075xID'), title: '0.75x'),
OptionData(key: const Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', isSelected: true),
OptionData(key: const Key('PlaybackOptionSpeed125xID'), title: '1.25x'),
OptionData(key: const Key('PlaybackOptionSpeed15xID'), title: '1.5x'),
OptionData(key: const Key('PlaybackOptionSpeed175xID'), title: '1.75x'),
OptionData(key: const Key('PlaybackOptionSpeed2xID'), title: '2x'),
];
final List<OptionData> _qualityOptions = [
OptionData(key: const Key('PlaybackOptionQualityAutoID'), title: 'Авто', isSelected: true),
OptionData(key: const Key('PlaybackOptionQuality4kID'), title: '4K'),
OptionData(key: const Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD'),
OptionData(key: const Key('PlaybackOptionQuality1080pID'), title: '1080p FHD'),
OptionData(key: const Key('PlaybackOptionQuality720pID'), title: '720p HD'),
OptionData(key: const Key('PlaybackOptionQuality480pID'), title: '480p'),
OptionData(key: const Key('PlaybackOptionQuality360pID'), title: '360p'),
OptionData(key: const Key('PlaybackOptionQuality240pID'), title: '240p'),
OptionData(key: const Key('PlaybackOptionQuality144pID'), title: '144p')
];
final List<OptionData> _qualityNamingOptions = [
OptionData(key: const Key('PlaybackOptionQualityNameCommonID'), title: 'Common', isSelected: true),
OptionData(key: const Key('PlaybackOptionQualityNameRusID'), title: 'Russian'),
OptionData(key: const Key('PlaybackOptionQualityNameEngID'), title: 'English'),
OptionData(key: const Key('PlaybackOptionQualityNameResolutionID'), title: 'Resolution')
];
final List<NumericOptionData> _timeoutsOptions = [
NumericOptionData(key: const Key('TimeoutsOptionManifestID'), title: 'Манифест (мс)', value: 5000),
NumericOptionData(key: const Key('TimeoutsOptionTrackID'), title: 'Трек (мс)', value: 3000),
NumericOptionData(key: const Key('TimeoutsOptionChunkID'), title: 'Сегмент (мс)', value: 3000)
];
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: const CupertinoNavigationBar(
key: Key('SettingsAppBarID'),
middle: Text('Настройки', style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
padding: EdgeInsetsDirectional.all(0),
navigationBar: CupertinoNavigationBar(
key: const Key('SettingsAppBarID'),
middle: const Text('Настройки', style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.black, fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(
previousPageTitle: 'Назад',
onPressed: () {
final bloc = context.read<SettingsBloc>();
bloc.add(DismissEvent());
}
),
padding: const EdgeInsetsDirectional.all(0),
backgroundColor: CupertinoColors.white,
),
child: SafeArea(
child: SingleChildScrollView(
child: Column(
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
final bloc = context.read<SettingsBloc>();
return Column(
children: [
// НАСТРОЙКИ СКИНА
CupertinoListSection.insetGrouped(
@@ -140,14 +42,10 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Выбор скина'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: <Widget>[..._skinOptions.map((value) {
return CheckmarkListOption(
value, () {},
);
}).toList()]
children: _buildSkinSettings(bloc)
),
// НАСТРОЙКИ СТАНДАРТНОГО СКИНА
@@ -156,18 +54,10 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Настройки стандартного скина'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: <Widget>[..._standardSkinOptions.map((value) {
if (value.key == const Key('SkinStandardOptionFullscreenID')) {
return OptionsView(value.title, _fullscreen);
} else if (value.key == const Key('SkinStandardOptionColorID')) {
return OptionsView(value.title, _colors);
} else {
return ToggleView(value);
}
}).toList()]
children: _buildSettingsWidgets(SettingsInitialState.standardSkinSettings, bloc)
),
// ВОСПРОИЗВЕДЕНИЕ
@@ -176,28 +66,10 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Воспроизведение'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: <Widget>[..._playbackOptions.map((value) {
if (value.key == const Key('PlaybackOptionVolumeID')) {
return OptionsView(value.title, _volumeOptions);
} else if (value.key == const Key('PlaybackOptionBrightnessID')) {
return OptionsView(value.title, _brightnessOptions);
} else if (value.key == const Key('PlaybackOptionSpeedID')) {
return OptionsView(value.title, _speedOptions);
} else if (value.key == const Key('PlaybackOptionQualityID')) {
return OptionsView(value.title, _qualityOptions);
} else if (value.key == const Key('PlaybackOptionQualityNamingID')) {
return OptionsView(value.title, _qualityNamingOptions);
} else if (value.key == const Key('PlaybackOptionStartPositionID')) {
return ToggleView(value);
} else {
return ToggleView(value);
}
}).toList()
+ [InputView(_startPositionOption)]
]
children: _buildSettingsWidgets(SettingsInitialState.playbackOptions, bloc)
),
// ДОПОЛНИТЕЛЬНО
@@ -206,16 +78,16 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Дополнительно'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
footer: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: const Text('Доступно только для видео в формате mp4, длительностью не более 5 мин.',
style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
),
),
children: [ToggleView(_extraOption)]
children: _buildSettingsWidgets([SettingsInitialState.extraOption], bloc)
),
// ЗАДЕРЖКИ
@@ -224,12 +96,10 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Настройка задержек (только для HLS)'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: <Widget>[..._timeoutsOptions.map((value) {
return InputView(value);
}).toList()]
children: _buildSettingsWidgets(SettingsInitialState.timeoutsOptions, bloc)
),
// Логирование
@@ -238,7 +108,7 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Логирование'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: [
@@ -248,49 +118,7 @@ class _SettingsViewState extends State<SettingsView> {
color: CupertinoColors.systemGrey3,
border: Border.all(color: CupertinoColors.systemGrey3)
),
child: CupertinoSlidingSegmentedControl<LogType>(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
groupValue: _currentLogType,
onValueChanged: (LogType? value) {
if (value != null) {
setState(() {
_currentLogType = value;
});
}
},
children: const <LogType, Widget>{
LogType.off: Padding(
key: Key('LogOffSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Выкл.',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
),
LogType.info: Padding(
key: Key('LogInfoSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Инфо',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
),
LogType.debug: Padding(
key: Key('LogDebugSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Отладка',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
)
},
)
child: _buildLog(context)
),
]
),
@@ -301,7 +129,7 @@ class _SettingsViewState extends State<SettingsView> {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Firebase'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: [
@@ -311,9 +139,8 @@ class _SettingsViewState extends State<SettingsView> {
alignment: Alignment.centerLeft,
borderRadius: const BorderRadius.all(Radius.circular(10)),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: const Text('Проверка падения приложения',
style: TextStyle(color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
onPressed: (){}
child: const Text('Проверка падения приложения', style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
onPressed: () { bloc.add(CrashEvent()); }
)
)
]
@@ -322,13 +149,145 @@ class _SettingsViewState extends State<SettingsView> {
// ВЕРСИЯ
Container(
margin: const EdgeInsets.only(top: 20),
child: const Text('VERSION 1.2.0 (1063)',
style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400))
child: FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
return _buildVersion(snapshot, bloc.state);
})
)
]
)
);
},
)
)
)
);
}
Widget _buildVersion(AsyncSnapshot<PackageInfo> packageInfo, SettingsState state) {
const textStyle = TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400);
if (packageInfo.hasData) {
var versions = [Text('ВЕРСИЯ ПРИЛОЖЕНИЯ ${packageInfo.requireData.version}', style: textStyle)];
final settingsState = state;
if (settingsState is! SettingsInitialState) { return Column(children: versions); }
versions.add(Text('ВЕРСИЯ NUT.PLAYER ${settingsState.playerVersion}', style: textStyle));
return Column(children: versions);
} else if (packageInfo.hasError) {
return Text(packageInfo.error.toString(), style: textStyle);
} else {
return _buildLoader();
}
}
Widget _buildLoader() {
return Container(
margin: const EdgeInsets.all(10),
width: double.infinity,
height: 80,
child: const CupertinoActivityIndicator(radius: 20),
);
}
List<Widget> _buildSkinSettings(SettingsBloc bloc) {
return <Widget>[...SettingsInitialState.skinSettings.map((setting) {
final isSelected = bloc.isSelected(setting);
return CheckmarkListOption(
setting.title,
isSelected, () {
final event = setting.onChange?.call(!isSelected);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
}).toList()];
}
List<Widget> _buildSettingsWidgets(List<Object> objects, SettingsBloc bloc) {
return <Widget>[...objects.map((setting) {
if (setting is OptionDataContainer) {
final selectedIndex = bloc.selectedIndex(setting);
return OptionsView(
setting.title,
setting.options,
selectedIndex,
(selectedIndex) {
final selectedOption = setting.options[selectedIndex];
final event = setting.onSelectedOption?.call(selectedOption);
if (event != null) {
bloc.add(event);
}
},
key: setting.key,
);
} else if (setting is BoolOptionData) {
final isSelected = bloc.isSelected(setting);
return ToggleView(
title: setting.title,
isSelected: isSelected,
onChange: (isOn) {
final event = setting.onChange?.call(isOn);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else if (setting is NumericOptionData) {
final currentValue = bloc.currentNumericValue(setting);
return InputView(
setting.title,
currentValue,
(newTime) {
final event = setting.onChange?.call(newTime);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else {
return const Text('not implemented');
}
}).toList()];
}
Widget? _buildLog(BuildContext context) {
final bloc = context.read<SettingsBloc>();
final settingsState = bloc.state;
if (settingsState is! SettingsInitialState) { return null; }
return CupertinoSlidingSegmentedControl<LogType>(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
groupValue: settingsState.log,
onValueChanged: (LogType? value) {
if (value != null) {
bloc.add(LogTypeChangedEvent(value));
}
},
children: <LogType, Widget>{
LogType.off: _buildLogSegment(SettingsInitialState.logOptions[LogType.off.toString()]!),
LogType.info: _buildLogSegment(SettingsInitialState.logOptions[LogType.info.toString()]!),
LogType.debug: _buildLogSegment(SettingsInitialState.logOptions[LogType.debug.toString()]!),
},
);
}
Padding _buildLogSegment(OptionData data) {
return Padding(
key: data.key,
padding: const EdgeInsets.symmetric(vertical: 6),
child: Text(data.title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
);
}
}
@@ -1,31 +1,24 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class ToggleView extends StatefulWidget {
final OptionData data;
class ToggleView extends StatelessWidget {
final String title;
final bool isSelected;
final Function(bool)? onChange;
const ToggleView(this.data, {super.key});
@override
State<ToggleView> createState() => _ToggleViewState();
}
class _ToggleViewState extends State<ToggleView> {
const ToggleView({super.key, required this.title, required this.isSelected, this.onChange});
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: widget.data.key,
key: key,
trailing: CupertinoSwitch(
value: widget.data.isSelected,
value: isSelected,
activeColor: CupertinoColors.activeGreen,
onChanged: (bool value) {
setState(() {
widget.data.isSelected = value;
});
onChange?.call(value);
},
),
title: Text(widget.data.title, style: const TextStyle(fontSize: 17))
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
);
}
}
+252 -17
View File
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: d84d98f1992976775f83083523a34c5d22fea191eec3abb2bd09537fb623c2e0
url: "https://pub.dev"
source: hosted
version: "1.3.7"
async:
dependency: transitive
description:
@@ -9,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
url: "https://pub.dev"
source: hosted
version: "8.1.2"
boolean_selector:
dependency: transitive
description:
@@ -37,10 +53,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.18.0"
cupertino_icons:
dependency: "direct main"
description:
@@ -65,11 +81,83 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "95580fa07c8ca3072a2bb1fecd792616a33f8683477d25b7d29d3a6a399e6ece"
url: "https://pub.dev"
source: hosted
version: "2.17.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2
url: "https://pub.dev"
source: hosted
version: "4.8.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: e8c408923cd3a25bd342c576a114f2126769cd1a57106a4edeaa67ea4a84e962
url: "https://pub.dev"
source: hosted
version: "2.8.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "833cf891d10e5e819a2034048ff7e8882bcc0b51055c0e17f5fe3f3c3c177a9d"
url: "https://pub.dev"
source: hosted
version: "3.3.7"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: dfdf1172f35fc0b0132bc5ec815aed52c07643ee56732e6807ca7dc12f7fce86
url: "https://pub.dev"
source: hosted
version: "3.6.7"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: "4ceb092cd14c3bce0dc8cd4046754cd1e5e5c1977155e286b512b3f84fe1c03e"
url: "https://pub.dev"
source: hosted
version: "11.2.8"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: "88f8b8bb7181eef125536297f11b28171f59d2f53a59610e3966c2e38be3de4c"
url: "https://pub.dev"
source: hosted
version: "4.4.7"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: bd05589cf17a8d5e2a90bdffcdab434c97e27bc37ca0056bfa90accf0366a7b0
url: "https://pub.dev"
source: hosted
version: "3.6.8"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
url: "https://pub.dev"
source: hosted
version: "8.1.3"
flutter_driver:
dependency: transitive
description: flutter
@@ -88,16 +176,53 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
keyboard_actions:
dependency: "direct main"
description:
name: keyboard_actions
sha256: "31e0ab2a706ac8f58887efa60efc1f19aecdf37d8ab0f665a0f156d1fbeab650"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
lints:
dependency: transitive
description:
@@ -126,10 +251,18 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nut_player:
dependency: "direct main"
description:
@@ -137,6 +270,20 @@ packages:
relative: true
source: path
version: "0.0.1"
nut_player_android:
dependency: transitive
description:
path: "../../nut_player_android"
relative: true
source: path
version: "0.0.1"
nut_player_ios:
dependency: transitive
description:
path: "../../nut_player_ios"
relative: true
source: path
version: "0.0.1"
nut_player_platform_interface:
dependency: transitive
description:
@@ -144,6 +291,22 @@ packages:
relative: true
source: path
version: "0.0.1"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
path:
dependency: transitive
description:
@@ -156,10 +319,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.2"
plugin_platform_interface:
dependency: transitive
description:
@@ -176,6 +339,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.4"
provider:
dependency: transitive
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
url: "https://pub.dev"
source: hosted
version: "6.0.5"
screen_brightness:
dependency: "direct main"
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
sky_engine:
dependency: transitive
description: flutter
@@ -193,18 +412,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
@@ -233,10 +452,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
@@ -249,18 +476,18 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
url: "https://pub.dev"
source: hosted
version: "11.7.1"
version: "11.10.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.3.0"
webdriver:
dependency: transitive
description:
@@ -269,6 +496,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
win32:
dependency: transitive
description:
name: win32
sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
sdks:
dart: ">=3.1.2 <4.0.0"
dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.3.0"
+8 -1
View File
@@ -16,6 +16,10 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
package_info_plus: ^4.1.0
keyboard_actions: ^4.2.0
screen_brightness: ^0.2.2
nut_player:
# When depending on this package from a real application you should use:
@@ -28,6 +32,9 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
firebase_core: ^2.17.0
firebase_storage: ^11.2.8
firebase_crashlytics: ^3.3.7
dev_dependencies:
integration_test:
@@ -47,7 +54,6 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
@@ -56,6 +62,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- assets/skinIcons/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
+15 -2
View File
@@ -1,2 +1,15 @@
export 'src/legacy/closed_caption_file.dart';
export 'src/legacy/video_player_value.dart';
export 'src/legacy/video_player_value.dart';
export 'src/legacy/video_scrubber.dart';
export 'src/legacy/video_progress_indicator.dart';
// new one
export 'src/widget/video_player.dart';
export 'src/controller/video_player_controller.dart';
export 'src/player_version_observer.dart';
//
export 'src/provider/provider.dart';
export 'src/provider/common_provider.dart';
export 'src/provider/json_provider/json_provider.dart';
export 'src/model/content_type.dart';
export 'src/model/player_content.dart';
export 'src/model/player_statistic_record.dart';
export 'src/model/player_subtitle_record.dart';
@@ -0,0 +1,423 @@
import 'package:flutter/services.dart';
import 'package:nut_player/src/provider/common_provider.dart';
import 'package:nut_player/src/provider/provider.dart';
import '../legacy/video_player_value.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart'
show PlayerId, VideoEvent, VideoEventType, PlatformHTTPMethod, PlatformSubtitleType;
import '../model/content_type.dart';
import '../model/platform_player_content_impl.dart';
import '../model/player_statistic_record.dart';
import '../model/player_subtitle_record.dart';
import '../platform/nut_player_platform.dart';
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
// MARK: - Private properties
Provider _provider;
PlayerId? _playerId;
Timer? _timer;
bool _isDisposed = false;
bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
Completer<void>? _creationHandler;
_VideoAppLifeCycleObserver? _lifeCycleObserver;
StreamSubscription<VideoEvent>? _eventSubscription;
// MARK: - Constructors
// Private constructor
VideoPlayerController._(this._provider, super.value);
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
final playerId = _playerId;
if (playerId != null) {
final completionHandler = _creationHandler;
if (completionHandler != null) {
await completionHandler.future;
}
_timer?.cancel();
_timer = null;
await _eventSubscription?.cancel();
_eventSubscription = null;
await nutPlayerPlatform.dispose(playerId);
_lifeCycleObserver?.dispose();
_lifeCycleObserver = null;
}
_isDisposed = true;
super.dispose();
}
// MARK: - Factories
factory VideoPlayerController.network(String urlPath, {bool isLive = false}) {
return VideoPlayerController._(
CommonProvider.url(urlPath, isLive: isLive),
const VideoPlayerValue(duration: Duration.zero)
);
}
factory VideoPlayerController.content({
required ContentType content,
List<PlayerStatisticRecord> statistics = const [],
List<PlayerSubtitleRecord> subtitles = const []
}) {
return VideoPlayerController._(CommonProvider.content(
content: content, statistics: statistics, subtitles: subtitles),
const VideoPlayerValue(duration: Duration.zero)
);
}
factory VideoPlayerController.provider(Provider provider) {
return VideoPlayerController._(provider, const VideoPlayerValue(duration: Duration.zero));
}
// MARK: - Public properties
PlayerId? get playerId => _playerId;
Stream<String> get logStream => nutPlayerPlatform.logStream();
Stream<Object> get skinActionStream => nutPlayerPlatform.skinActionStream();
/// The position in the current video.
Future<Duration?> get position async {
final playerId = _playerId;
if (_isDisposed || playerId == null) {
return null;
}
return nutPlayerPlatform.getPosition(playerId);
}
// MARK: - Public methods
@override
void removeListener(VoidCallback listener) {
// Prevent VideoPlayer from causing an exception to be thrown when attempting to
// remove its own listener after the controller has already been disposed.
if (!_isDisposed) {
super.removeListener(listener);
}
}
Future<void> initialize({Map<String, Object>? params}) async {
final bool allowBackgroundPlayback;
if (params != null && params["enablePip"] is bool) {
allowBackgroundPlayback = params["enablePip"] as bool;
} else {
allowBackgroundPlayback = false;
}
if (!allowBackgroundPlayback && _lifeCycleObserver == null) {
_lifeCycleObserver = _VideoAppLifeCycleObserver(this);
_lifeCycleObserver?.initialize();
}
final playerContent = await _provider.retrieveContent();
final platformContent = PlatformPlayerContentImpl(
content: createPlatformContent(playerContent.content),
statistics: playerContent.statistics.map((record) => PlatformStatisticRecordImpl(
name: record.name,
urlTemplate: record.urlTemplate,
start: record.start,
delay: record.delay,
count: record.count,
method: record.method == HTTPMethod.get ? PlatformHTTPMethod.get : PlatformHTTPMethod.post,
body: record.body
)).toList(),
subtitles: playerContent.subtitles.map((record) => PlatformSubtitleRecordImpl(
title: record.title,
type: PlatformSubtitleType.srt,
url: record.url,
language: record.language,
)).toList()
);
final playerId = await nutPlayerPlatform.create(content: platformContent, params: params);
_playerId = playerId;
_creationHandler?.complete(null);
_creationHandler = null;
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
if (_isDisposed) {
return;
}
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(
isInitialized: true,
isCompleted: false,
);
initializingCompleter.complete(null);
break;
case VideoEventType.ready:
value = value.copyWith(
duration: event.duration,
size: event.size,
subtitles: event.subtitles,
rotationCorrection: event.rotationCorrection,
errorDescription: null,
);
_applyVolume();
break;
case VideoEventType.completed:
pause().then((void pauseResult) => seek(value.duration));
value = value.copyWith(isCompleted: true);
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
break;
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
break;
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
if (event.isPlaying ?? false) {
value = value.copyWith(isPlaying: event.isPlaying, isCompleted: false);
} else {
value = value.copyWith(isPlaying: event.isPlaying);
}
break;
case VideoEventType.didFetchQualities:
value = value.copyWith(qualities: event.qualities);
break;
case VideoEventType.unknown:
break;
}
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
_eventSubscription = nutPlayerPlatform
.videoEventsFor(playerId)
.listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
/// Play video.
Future<void> play() async {
if (value.position == value.duration) {
await seek(Duration.zero);
}
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
/// Завершение видео.
Future<void> end() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.end(playerId);
}
/// Seek video.
Future<void> seek(Duration position) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await nutPlayerPlatform.seek(playerId, position);
_updatePosition(position);
}
Future<void> setVolume(double volume) async {
value = value.copyWith(volume: volume.clamp(0.0, 1.0));
await _applyVolume();
}
/// Sets subtitle by the id.
Future<void> setSubtitles(String subtitleID) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.setSubtitle(playerId, subtitleID);
}
// Установить качество по идентификатору
Future<void> setQuality(String qualityID) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.setQuality(playerId, qualityID);
}
Future<void> setLog(String limitOutputLevel) async {
await nutPlayerPlatform.setLimitOutputLevel(limitOutputLevel);
}
Future<void> setBrightness(double brightness) async {
await ScreenBrightness().setScreenBrightness(brightness);
}
Future<void> setPlaybackSpeed(double speed) async {
if (speed < 0) {
throw ArgumentError.value(
speed,
'Negative playback speeds are generally unsupported.',
);
} else if (speed == 0) {
throw ArgumentError.value(
speed,
'Zero playback speed is generally unsupported. Consider using [pause].',
);
}
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
// MARK: - Private methods
Future<void> _applyPlayPause() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
if (value.isPlaying) {
await nutPlayerPlatform.play(playerId);
// Cancel previous timer.
_timer?.cancel();
_timer = Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
if (_isDisposed) {
return;
}
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
},
);
// This ensures that the correct playback speed is always applied when
// playing back. This is necessary because we do not set playback speed
// when paused.
await _applyPlaybackSpeed();
} else {
_timer?.cancel();
await nutPlayerPlatform.pause(playerId);
}
}
Future<void> _applyPlaybackSpeed() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
// Setting the playback speed on iOS will trigger the video to play. We
// prevent this from happening by not applying the playback speed until
// the video is manually played from Flutter.
if (!value.isPlaying) {
return;
}
await nutPlayerPlatform.setPlaybackSpeed(playerId, value.playbackSpeed);
}
Future<void> _applyVolume() async {
final playerId = _playerId;
final volume = value.volume;
if (_isDisposedOrNotInitialized ||
playerId == null ||
volume == null) {
return;
}
await nutPlayerPlatform.setVolume(playerId, volume);
}
void _updatePosition(Duration position) {
value = value.copyWith(
position: position,
isCompleted: position == value.duration,
);
}
}
class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
_VideoAppLifeCycleObserver(this._controller);
bool _wasPlayingBeforePause = false;
final VideoPlayerController _controller;
void initialize() {
final value = _ambiguate(WidgetsBinding.instance);
if (value != null) {
value.addObserver(this);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_wasPlayingBeforePause = _controller.value.isPlaying;
_controller.pause();
} else if (state == AppLifecycleState.resumed) {
if (_wasPlayingBeforePause) {
_controller.play();
}
}
}
void dispose() {
final value = _ambiguate(WidgetsBinding.instance);
if (value != null) {
value.removeObserver(this);
}
}
}
T? _ambiguate<T>(T? value) => value;
@@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
/// Widget for displaying closed captions on top of a video.
///
/// If [text] is null, this widget will not display anything.
///
/// If [textStyle] is supplied, it will be used to style the text in the closed
/// caption.
///
/// Note: in order to have closed captions, you need to specify a
/// [VideoPlayerController.closedCaptionFile].
///
/// Usage:
///
/// ```dart
/// Stack(children: <Widget>[
/// VideoPlayer(_controller),
/// ClosedCaption(text: _controller.value.caption.text),
/// ]),
/// ```
class ClosedCaption extends StatelessWidget {
/// Creates a a new closed caption, designed to be used with
/// [VideoPlayerValue.caption].
///
/// If [text] is null or empty, nothing will be displayed.
const ClosedCaption({super.key, this.text, this.textStyle});
/// The text that will be shown in the closed caption, or null if no caption
/// should be shown.
/// If the text is empty the caption will not be shown.
final String? text;
/// Specifies how the text in the closed caption should look.
///
/// If null, defaults to [DefaultTextStyle.of(context).style] with size 36
/// font colored white.
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
final String? text = this.text;
if (text == null || text.isEmpty) {
return const SizedBox.shrink();
}
final TextStyle effectiveTextStyle = textStyle ??
DefaultTextStyle.of(context).style.copyWith(
fontSize: 36.0,
color: Colors.white,
);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xB8000000),
borderRadius: BorderRadius.circular(2.0),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: Text(text, style: effectiveTextStyle),
),
),
),
);
}
}
@@ -1,86 +0,0 @@
import 'package:flutter/foundation.dart' show immutable, objectRuntimeType;
/// A structured representation of a parsed closed caption file.
///
/// A closed caption file includes a list of captions, each with a start and end
/// time for when the given closed caption should be displayed.
///
/// The [captions] are a list of all captions in a file, in the order that they
/// appeared in the file.
///
/// See:
/// * [SubRipCaptionFile].
/// * [WebVTTCaptionFile].
abstract class ClosedCaptionFile {
/// The full list of captions from a given file.
///
/// The [captions] will be in the order that they appear in the given file.
List<Caption> get captions;
}
/// A representation of a single caption.
///
/// A typical closed captioning file will include several [Caption]s, each
/// linked to a start and end time.
@immutable
class Caption {
/// Creates a new [Caption] object.
///
/// This is not recommended for direct use unless you are writing a parser for
/// a new closed captioning file type.
const Caption({
required this.number,
required this.start,
required this.end,
required this.text,
});
/// The number that this caption was assigned.
final int number;
/// When in the given video should this [Caption] begin displaying.
final Duration start;
/// When in the given video should this [Caption] be dismissed.
final Duration end;
/// The actual text that should appear on screen to be read between [start]
/// and [end].
final String text;
/// A no caption object. This is a caption with [start] and [end] durations of zero,
/// and an empty [text] string.
static const Caption none = Caption(
number: 0,
start: Duration.zero,
end: Duration.zero,
text: '',
);
@override
String toString() {
return '${objectRuntimeType(this, 'Caption')}('
'number: $number, '
'start: $start, '
'end: $end, '
'text: $text)';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Caption &&
runtimeType == other.runtimeType &&
number == other.number &&
start == other.start &&
end == other.end &&
text == other.text;
@override
int get hashCode => Object.hash(
number,
start,
end,
text,
);
}
@@ -1,522 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:io';
import 'dart:async';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart'
show DataSource, NutPlayerPlatform, PlayerId, DataSourceType, VideoFormat, VideoEvent, VideoEventType;
import 'dart:math' as math;
import 'video_player_value.dart';
import 'closed_caption_file.dart';
NutPlayerPlatform? _lastNutPlayerPlatform;
NutPlayerPlatform get _nutPlayerPlatform {
final NutPlayerPlatform currentInstance = NutPlayerPlatform.instance;
if (_lastNutPlayerPlatform != currentInstance) {
// This will clear all open videos on the platform when a full restart is
// performed.
currentInstance.init();
_lastNutPlayerPlatform = currentInstance;
}
return currentInstance;
}
/// Widget that displays the video controlled by [controller].
class VideoPlayer extends StatefulWidget {
/// Uses the given [controller] for all video rendered in this widget.
const VideoPlayer(this.controller, {super.key});
/// The [VideoPlayerController] responsible for the video being rendered in
/// this widget.
final VideoPlayerController controller;
@override
State<VideoPlayer> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
_VideoPlayerState() {
_listener = () {
final int newPlayerId = widget.controller.playerId;
if (newPlayerId != _playerId) {
setState(() {
_playerId = newPlayerId;
});
}
};
}
late VoidCallback _listener;
late PlayerId _playerId;
@override
void initState() {
super.initState();
_playerId = widget.controller.playerId;
// Need to listen for initialization events since the actual texture ID
// becomes available after asynchronous initialization finishes.
widget.controller.addListener(_listener);
}
@override
void didUpdateWidget(VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.controller.removeListener(_listener);
_playerId = widget.controller.playerId;
widget.controller.addListener(_listener);
}
@override
void deactivate() {
super.deactivate();
widget.controller.removeListener(_listener);
}
@override
Widget build(BuildContext context) {
return _playerId == VideoPlayerController.kUninitializedPlayerId
? Container()
: _VideoPlayerWithRotation(
rotation: widget.controller.value.rotationCorrection,
child: _nutPlayerPlatform.buildView(_playerId),
);
}
}
class _VideoPlayerWithRotation extends StatelessWidget {
const _VideoPlayerWithRotation({required this.rotation, required this.child});
final int rotation;
final Widget child;
@override
Widget build(BuildContext context) => rotation == 0
? child
: Transform.rotate(
angle: rotation * math.pi / 180,
child: child,
);
}
/// Controls a platform video player, and provides updates when the state is
/// changing.
///
/// Instances must be initialized with initialize.
///
/// The video is displayed in a Flutter app by creating a [VideoPlayer] widget.
///
/// To reclaim the resources used by the player call [dispose].
///
/// After [dispose] all further calls are ignored.
/*
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
/// Constructs a [VideoPlayerController] playing a video from an asset.
///
/// The name of the asset is given by the [dataSource] argument and must not be
/// null. The [package] argument must be non-null when the asset comes from a
/// package and null otherwise.
VideoPlayerController.asset(this.dataSource,
{this.package,
Future<ClosedCaptionFile>? closedCaptionFile})
: _closedCaptionFileFuture = closedCaptionFile,
dataSourceType = DataSourceType.asset,
format = null,
httpHeaders = const <String, String>{},
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a network video.
///
/// The URI for the video is given by the [dataSource] argument.
///
/// **Android only**: The [formatHint] option allows the caller to override
/// the video format detection code.
///
/// [httpHeaders] option allows to specify HTTP headers
/// for the request to the [dataSource].
VideoPlayerController.network(
this.dataSource, {
this.format,
Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{},
}) : _closedCaptionFileFuture = closedCaptionFile,
dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a network video.
///
/// The URI for the video is given by the [dataSource] argument.
///
/// **Android only**: The [formatHint] option allows the caller to override
/// the video format detection code.
///
/// [httpHeaders] option allows to specify HTTP headers
/// for the request to the [dataSource].
VideoPlayerController.networkUrl(
Uri url, {
this.format,
Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{},
}) : _closedCaptionFileFuture = closedCaptionFile,
dataSource = url.toString(),
dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a video from a file.
///
/// This will load the file from a file:// URI constructed from [file]'s path.
/// [httpHeaders] option allows to specify HTTP headers, mainly used for hls files like (m3u8).
VideoPlayerController.file(File file,
{Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{}})
: _closedCaptionFileFuture = closedCaptionFile,
dataSource = Uri.file(file.absolute.path).toString(),
dataSourceType = DataSourceType.file,
package = null,
format = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a video from a contentUri.
///
/// This will load the video from the input content-URI.
/// This is supported on Android only.
VideoPlayerController.contentUri(Uri contentUri,
{Future<ClosedCaptionFile>? closedCaptionFile})
: assert(defaultTargetPlatform == TargetPlatform.android,
'VideoPlayerController.contentUri is only supported on Android.'),
_closedCaptionFileFuture = closedCaptionFile,
dataSource = contentUri.toString(),
dataSourceType = DataSourceType.contentUri,
package = null,
format = null,
httpHeaders = const <String, String>{},
super(const VideoPlayerValue(duration: Duration.zero));
/// The URI to the video file. This will be in different formats depending on
/// the [DataSourceType] of the original video.
final String dataSource;
/// HTTP headers used for the request to the [dataSource].
/// Only for [VideoPlayerController.network].
/// Always empty for other video types.
final Map<String, String> httpHeaders;
/// **Android only**. Will override the platform's generic file format
/// detection with whatever is set here.
final VideoFormat? format;
/// Describes the type of data source this [VideoPlayerController]
/// is constructed with.
final DataSourceType dataSourceType;
/// Only set for [asset] videos. The package that the asset was loaded from.
final String? package;
Future<ClosedCaptionFile>? _closedCaptionFileFuture;
ClosedCaptionFile? _closedCaptionFile;
Timer? _timer;
bool _isDisposed = false;
Completer<void>? _creatingCompleter;
StreamSubscription<dynamic>? _eventSubscription;
_VideoAppLifeCycleObserver? _lifeCycleObserver;
/// The id of a texture that hasn't been initialized.
@visibleForTesting
static const PlayerId kUninitializedPlayerId = -1;
PlayerId _playerId = kUninitializedPlayerId;
/// This is just exposed for testing. It shouldn't be used by anyone depending
/// on the plugin.
@visibleForTesting
PlayerId get playerId => _playerId;
/// Attempts to open the given [dataSource] and load metadata about the video.
Future<void> initialize() async {
const bool allowBackgroundPlayback = false;
if (!allowBackgroundPlayback) {
_lifeCycleObserver = _VideoAppLifeCycleObserver(this);
}
_lifeCycleObserver?.initialize();
_creatingCompleter = Completer<void>();
late DataSource dataSourceDescription;
switch (dataSourceType) {
case DataSourceType.asset:
dataSourceDescription = DataSource(
sourceType: DataSourceType.asset,
format: VideoFormat.other,
asset: dataSource,
package: package,
);
break;
case DataSourceType.network:
dataSourceDescription = DataSource(
sourceType: DataSourceType.network,
uri: dataSource,
format: format ?? VideoFormat.other,
httpHeaders: httpHeaders,
);
break;
case DataSourceType.file:
dataSourceDescription = DataSource(
sourceType: DataSourceType.file,
format: VideoFormat.other,
uri: dataSource,
httpHeaders: httpHeaders,
);
break;
case DataSourceType.contentUri:
dataSourceDescription = DataSource(
sourceType: DataSourceType.contentUri,
format: VideoFormat.other,
uri: dataSource,
);
break;
}
_playerId = (await _nutPlayerPlatform.create(dataSourceDescription));
_creatingCompleter?.complete(null);
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
if (_isDisposed) {
return;
}
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(
duration: event.duration,
size: event.size,
rotationCorrection: event.rotationCorrection,
isInitialized: event.duration != null,
errorDescription: null,
);
initializingCompleter.complete(null);
_applyVolume();
_applyPlayPause();
break;
case VideoEventType.completed:
// In this case we need to stop _timer, set isPlaying=false, and
// position=value.duration. Instead of setting the values directly,
// we use pause() and seekTo() to ensure the platform stops playing
// and seeks to the last frame of the video.
pause().then((void pauseResult) => seekTo(value.duration));
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
break;
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
break;
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
value = value.copyWith(isPlaying: event.isPlaying);
break;
case VideoEventType.unknown:
break;
}
}
if (_closedCaptionFileFuture != null) {
await _updateClosedCaptionWithFuture(_closedCaptionFileFuture);
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
_eventSubscription = _nutPlayerPlatform
.videoEventsFor(_playerId)
.listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
if (_creatingCompleter != null) {
await _creatingCompleter!.future;
if (!_isDisposed) {
_isDisposed = true;
_timer?.cancel();
await _eventSubscription?.cancel();
await _nutPlayerPlatform.dispose(_playerId);
}
_lifeCycleObserver?.dispose();
}
_isDisposed = true;
super.dispose();
}
/// Starts playing the video.
///
/// If the video is at the end, this method starts playing from the beginning.
///
/// This method returns a future that completes as soon as the "play" command
/// has been sent to the platform, not when playback itself is totally
/// finished.
Future<void> play() async {
if (value.position == value.duration) {
await seekTo(Duration.zero);
}
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
Future<void> _applyPlayPause() async {
if (_isDisposedOrNotInitialized) {
return;
}
if (value.isPlaying) {
await _nutPlayerPlatform.play(_playerId);
// Cancel previous timer.
_timer?.cancel();
_timer = Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
if (_isDisposed) {
return;
}
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
},
);
// This ensures that the correct playback speed is always applied when
// playing back. This is necessary because we do not set playback speed
// when paused.
await _applyPlaybackSpeed();
} else {
_timer?.cancel();
await _nutPlayerPlatform.pause(_playerId);
}
}
Future<void> _applyVolume() async {
if (_isDisposedOrNotInitialized) {
return;
}
await _nutPlayerPlatform.setVolume(_playerId, value.volume);
}
Future<void> _applyPlaybackSpeed() async {
if (_isDisposedOrNotInitialized) {
return;
}
// Setting the playback speed on iOS will trigger the video to play. We
// prevent this from happening by not applying the playback speed until
// the video is manually played from Flutter.
if (!value.isPlaying) {
return;
}
await _nutPlayerPlatform.setPlaybackSpeed(
_playerId,
value.playbackSpeed,
);
}
/// The position in the current video.
Future<Duration?> get position async {
if (_isDisposed) {
return null;
}
return _nutPlayerPlatform.getPosition(_playerId);
}
/// Sets the video's current timestamp to be at [moment]. The next
/// time the video is played it will resume from the given [moment].
///
/// If [moment] is outside of the video's full range it will be automatically
/// and silently clamped.
Future<void> seekTo(Duration position) async {
if (_isDisposedOrNotInitialized) {
return;
}
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await _nutPlayerPlatform.seek(_playerId, position);
_updatePosition(position);
}
/// Sets the audio volume of [this].
///
/// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a
/// linear scale.
Future<void> setVolume(double volume) async {
value = value.copyWith(volume: volume.clamp(0.0, 1.0));
await _applyVolume();
}
/// Sets the playback speed of [this].
///
/// [speed] indicates a speed value with different platforms accepting
/// different ranges for speed values. The [speed] must be greater than 0.
///
/// The values will be handled as follows:
/// * On web, the audio will be muted at some speed when the browser
/// determines that the sound would not be useful anymore. For example,
/// "Gecko mutes the sound outside the range `0.25` to `5.0`" (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate).
/// * On Android, some very extreme speeds will not be played back accurately.
/// Instead, your video will still be played back, but the speed will be
/// clamped by ExoPlayer (but the values are allowed by the player, like on
/// web).
/// * On iOS, you can sometimes not go above `2.0` playback speed on a video.
/// An error will be thrown for if the option is unsupported. It is also
/// possible that your specific video cannot be slowed down, in which case
/// the plugin also reports errors.
Future<void> setPlaybackSpeed(double speed) async {
if (speed < 0) {
throw ArgumentError.value(
speed,
'Negative playback speeds are generally unsupported.',
);
} else if (speed == 0) {
throw ArgumentError.value(
speed,
'Zero playback speed is generally unsupported. Consider using [pause].',
);
}
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
/// Sets the caption offset.
///
/// The [offset] will be used when getting the correct caption for a specific position.
/// The [offset] can be positive or negative.
///
/// The values will be handled as follows:
/// * 0: This is the default behaviour. No offset will be applied.
/// * >0: The caption will have a negative offset. So you will get caption text from the past.
/// * <0: The caption will have a positive offset. So you will get caption text from the future.
void setCaptionOffset(Duration offset) {
value = value.copyWith(
captionOffset: offset,
@@ -524,13 +13,6 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
);
}
/// The closed caption based on the current [position] in the video.
///
/// If there are no closed captions at the current [position], this will
/// return an empty [Caption].
///
/// If no [closedCaptionFile] was specified, this will always return an empty
/// [Caption].
Caption _getCaptionAt(Duration position) {
if (_closedCaptionFile == null) {
return Caption.none;
@@ -568,56 +50,4 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
_closedCaptionFile = await closedCaptionFile;
value = value.copyWith(caption: _getCaptionAt(value.position));
}
void _updatePosition(Duration position) {
value = value.copyWith(
position: position,
caption: _getCaptionAt(position),
);
}
@override
void removeListener(VoidCallback listener) {
// Prevent VideoPlayer from causing an exception to be thrown when attempting to
// remove its own listener after the controller has already been disposed.
if (!_isDisposed) {
super.removeListener(listener);
}
}
bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
}
///
class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
_VideoAppLifeCycleObserver(this._controller);
bool _wasPlayingBeforePause = false;
final VideoPlayerController _controller;
void initialize() {
_ambiguate(WidgetsBinding.instance)!.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_wasPlayingBeforePause = _controller.value.isPlaying;
_controller.pause();
} else if (state == AppLifecycleState.resumed) {
if (_wasPlayingBeforePause) {
_controller.play();
}
}
}
void dispose() {
_ambiguate(WidgetsBinding.instance)!.removeObserver(this);
}
}
/// This allows a value of type T or T? to be treated as a value of type T?.
///
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` on the stable branch.
T? _ambiguate<T>(T? value) => value;
}*/
@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show DurationRange;
import 'closed_caption_file.dart';
/// The duration, current position, buffering state, error state and settings
/// of a [VideoPlayerController].
@@ -13,17 +12,18 @@ class VideoPlayerValue {
required this.duration,
this.size = Size.zero,
this.position = Duration.zero,
this.caption = Caption.none,
this.captionOffset = Duration.zero,
this.subtitles,
this.qualities,
this.buffered = const <DurationRange>[],
this.isInitialized = false,
this.isPlaying = false,
this.isLooping = false,
this.isBuffering = false,
this.volume = 1.0,
this.volume,
this.playbackSpeed = 1.0,
this.rotationCorrection = 0,
this.errorDescription,
this.isCompleted = false,
});
/// Returns an instance for a video that hasn't been loaded.
@@ -49,16 +49,12 @@ class VideoPlayerValue {
/// The current playback position.
final Duration position;
/// The [Caption] that should be displayed based on the current [position].
///
/// This field will never be null. If there is no caption for the current
/// [position], this will be a [Caption.none] object.
final Caption caption;
/// Current available subtitles where key is the id of the subtitles and value is their title.
/// If no subtitles, the dictionary will be empty.
final Map<String, String>? subtitles;
/// The [Duration] that should be used to offset the current [position] to get the correct [Caption].
///
/// Defaults to Duration.zero.
final Duration captionOffset;
/// Текущие доступные качества видео. Если их нет, массив словарей будет пустым.
final List<Map<String, dynamic>>? qualities;
/// The currently buffered ranges.
final List<DurationRange> buffered;
@@ -73,7 +69,7 @@ class VideoPlayerValue {
final bool isBuffering;
/// The current volume of the playback.
final double volume;
final double? volume;
/// The current speed of the playback.
final double playbackSpeed;
@@ -83,6 +79,8 @@ class VideoPlayerValue {
/// If [hasError] is false this is `null`.
final String? errorDescription;
final bool isCompleted;
/// The [size] of the currently loaded video.
final Size size;
@@ -119,8 +117,8 @@ class VideoPlayerValue {
Duration? duration,
Size? size,
Duration? position,
Caption? caption,
Duration? captionOffset,
Map<String, String>? subtitles,
List<Map<String, dynamic>>? qualities,
List<DurationRange>? buffered,
bool? isInitialized,
bool? isPlaying,
@@ -130,13 +128,14 @@ class VideoPlayerValue {
double? playbackSpeed,
int? rotationCorrection,
String? errorDescription = _defaultErrorDescription,
bool? isCompleted,
}) {
return VideoPlayerValue(
duration: duration ?? this.duration,
size: size ?? this.size,
position: position ?? this.position,
caption: caption ?? this.caption,
captionOffset: captionOffset ?? this.captionOffset,
subtitles: subtitles ?? this.subtitles,
qualities: qualities ?? this.qualities,
buffered: buffered ?? this.buffered,
isInitialized: isInitialized ?? this.isInitialized,
isPlaying: isPlaying ?? this.isPlaying,
@@ -148,6 +147,7 @@ class VideoPlayerValue {
errorDescription: errorDescription != _defaultErrorDescription
? errorDescription
: this.errorDescription,
isCompleted: isCompleted ?? this.isCompleted,
);
}
@@ -157,8 +157,8 @@ class VideoPlayerValue {
'duration: $duration, '
'size: $size, '
'position: $position, '
'caption: $caption, '
'captionOffset: $captionOffset, '
'subtitles: $subtitles, '
'qualities: $qualities, '
'buffered: [${buffered.join(', ')}], '
'isInitialized: $isInitialized, '
'isPlaying: $isPlaying, '
@@ -166,7 +166,8 @@ class VideoPlayerValue {
'isBuffering: $isBuffering, '
'volume: $volume, '
'playbackSpeed: $playbackSpeed, '
'errorDescription: $errorDescription)';
'errorDescription: $errorDescription, '
'isCompleted: $isCompleted), ';
}
@override
@@ -176,8 +177,8 @@ class VideoPlayerValue {
runtimeType == other.runtimeType &&
duration == other.duration &&
position == other.position &&
caption == other.caption &&
captionOffset == other.captionOffset &&
subtitles == other.subtitles &&
listEquals(qualities, other.qualities) &&
listEquals(buffered, other.buffered) &&
isPlaying == other.isPlaying &&
isLooping == other.isLooping &&
@@ -187,14 +188,15 @@ class VideoPlayerValue {
errorDescription == other.errorDescription &&
size == other.size &&
rotationCorrection == other.rotationCorrection &&
isInitialized == other.isInitialized;
isInitialized == other.isInitialized &&
isCompleted == other.isCompleted;
@override
int get hashCode => Object.hash(
duration,
position,
caption,
captionOffset,
subtitles,
qualities,
buffered,
isPlaying,
isLooping,
@@ -205,5 +207,6 @@ class VideoPlayerValue {
size,
rotationCorrection,
isInitialized,
isCompleted,
);
}
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'video_progress_colors.dart';
import 'video_player_controller.dart';
import '../controller/video_player_controller.dart';
import 'video_scrubber.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show DurationRange;
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'video_player_controller.dart';
import '../controller/video_player_controller.dart';
/// A scrubber to control [VideoPlayerController]s
class VideoScrubber extends StatefulWidget {
@@ -35,7 +35,7 @@ class _VideoScrubberState extends State<VideoScrubber> {
final Offset tapPos = box.globalToLocal(globalPosition);
final double relative = tapPos.dx / box.size.width;
final Duration position = controller.value.duration * relative;
controller.seekTo(position);
controller.seek(position);
}
return GestureDetector(
@@ -0,0 +1,15 @@
import '../../nut_player.dart';
class CommonPlayerContent implements PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
CommonPlayerContent({required this.content, this.statistics = const [], this.subtitles = const []});
}
@@ -0,0 +1,27 @@
sealed class ContentType {}
final class AutoContentType extends ContentType {
final String urlPath;
AutoContentType({required this.urlPath});
}
final class HlsContentType extends ContentType {
final String urlPath;
final bool isLive;
HlsContentType({required this.urlPath, required this.isLive});
}
final class DashContentType extends ContentType {
final String urlPath;
DashContentType({required this.urlPath});
}
final class Mp4ContentType extends ContentType {
final String urlPath;
final bool isLoop;
Mp4ContentType({required this.urlPath, this.isLoop = false});
}
@@ -0,0 +1,62 @@
import 'package:nut_player/src/model/content_type.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart';
PlatformContentType createPlatformContent(ContentType type) {
final contentType = type;
if (contentType is AutoContentType) {
return PlatformAutoContentType(urlPath: contentType.urlPath);
} else if (contentType is HlsContentType) {
return PlatformHlsContentType(urlPath: contentType.urlPath, isLive: contentType.isLive);
} else if (contentType is DashContentType) {
return PlatformDashContentType(urlPath: contentType.urlPath);
} else if (contentType is Mp4ContentType) {
return PlatformMp4ContentType(urlPath: contentType.urlPath, isLoop: contentType.isLoop);
} else {
throw Error();
}
}
class PlatformPlayerContentImpl extends PlatformPlayerContent {
@override PlatformContentType content;
@override List<PlatformPlayerStatisticRecord> statistics;
@override List<PlatformPlayerSubtitleRecord> subtitles;
PlatformPlayerContentImpl({required this.content, required this.statistics, required this.subtitles});
}
class PlatformStatisticRecordImpl extends PlatformPlayerStatisticRecord {
@override String name;
@override String urlTemplate;
@override double start;
@override double delay;
@override int count;
@override PlatformHTTPMethod method;
@override String? body;
PlatformStatisticRecordImpl({
required this.name,
required this.urlTemplate,
required this.start,
required this.delay,
required this.count,
required this.method,
this.body
});
}
class PlatformSubtitleRecordImpl extends PlatformPlayerSubtitleRecord {
@override String title;
@override PlatformSubtitleType type;
@override String url;
@override String language;
PlatformSubtitleRecordImpl({
required this.title,
required this.type,
required this.url,
required this.language,
});
}
@@ -0,0 +1,9 @@
import './content_type.dart';
import './player_statistic_record.dart';
import './player_subtitle_record.dart';
abstract class PlayerContent {
ContentType get content;
List<PlayerStatisticRecord> get statistics;
List<PlayerSubtitleRecord> get subtitles;
}
@@ -0,0 +1,23 @@
enum HTTPMethod { get, post }
abstract class PlayerStatisticRecord {
/// Тип счетчика, определяет логику по которой будет запрашиваться счетчик.
/// Может быть несколько счетчиков с одинаковым типом, в этом случае их нужно запросить все.
String get name;
/// Ссылка для запроса счетчика.
/// Может содержать набор динамических параметров.
/// Ссылка может быть указана как относительная, без явного указания протокола (http/https).
/// В этом случае при запросе ссылки протокол нужно будет добавить.
String get urlTemplate;
/// Время в секундах когда нужно запросить счетчик во время проигрывания основного видео.
double get start;
/// Время в секундах между запросами счетчика во время проигрывания основного видео.
double get delay;
/// Максимальное кол-во запросов счетчика во время проигрывания основного видео.
int get count;
/// Метод запроса счетчика статистики.
HTTPMethod get method;
/// Содержимое запроса в случае отправки запроса через post.
/// Может содержать набор динамических параметров.
String? get body;
}
@@ -0,0 +1,14 @@
enum SubtitleType { srt, unknown }
abstract class PlayerSubtitleRecord {
/// Название субтитров.
/// Используется для вывода в меню плеера
String get title;
/// Тип субтитров
SubtitleType get type;
/// Ссылка на файл субтитров
String get url;
/// Язык на котором отображаются субтитры.
/// Соответствует стандарту ISO 639-2
String get language;
}
@@ -0,0 +1,14 @@
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show NutPlayerPlatform;
// Активная платформа плеера (android, ios, web)
NutPlayerPlatform? _lastNutPlayerPlatform;
NutPlayerPlatform get nutPlayerPlatform {
final NutPlayerPlatform currentInstance = NutPlayerPlatform.instance;
if (_lastNutPlayerPlatform != currentInstance) {
// This will clear all open videos on the platform when a full restart is
// performed.
currentInstance.init();
_lastNutPlayerPlatform = currentInstance;
}
return currentInstance;
}
@@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter/services.dart';
class PlayerVersionObserver {
final MethodChannel _pluginChannel = const MethodChannel('tech.nut/plugin');
final _versionStreamController = StreamController<String>();
Stream<String> get versionStream => _versionStreamController.stream.asBroadcastStream();
PlayerVersionObserver() {
_pluginChannel.setMethodCallHandler(_pluginVersionHandler);
_pluginChannel.invokeMethod("getPlayerVersion");
}
Future<dynamic> _pluginVersionHandler(MethodCall call) async {
switch (call.method) {
case 'pluginVersion':
final version = call.arguments['version'];
if (version != null) {
_versionStreamController.add(version);
}
break;
default:
break;
}
}
}
@@ -0,0 +1,38 @@
import 'package:nut_player/nut_player.dart';
import 'package:nut_player/src/model/common_player_content.dart';
class CommonProvider implements Provider {
final PlayerContent content;
// MARK: - Constructors
CommonProvider({required this.content});
factory CommonProvider.url(String urlPath, {bool isLive = false, bool isLoop = false}) {
final ContentType content;
if (urlPath.endsWith('m3u8')) {
content = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
content = Mp4ContentType(urlPath: urlPath, isLoop: isLoop);
}
final playerContent = CommonPlayerContent(content: content);
return CommonProvider(content: playerContent);
}
factory CommonProvider.content({
required ContentType content,
List<PlayerStatisticRecord> statistics = const [],
List<PlayerSubtitleRecord> subtitles = const []
}) {
final playerContent = CommonPlayerContent(content: content, statistics: statistics, subtitles: subtitles);
return CommonProvider(content: playerContent);
}
// MARK: - Provider
@override
Future<PlayerContent> retrieveContent() {
return Future.value(content);
}
}
@@ -0,0 +1,78 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:nut_player/nut_player.dart';
import 'package:nut_player/src/provider/json_provider/model/json_content.dart';
import 'package:nut_player/src/provider/json_provider/model/json_error.dart';
import 'package:nut_player/src/provider/json_provider/model/json_statistics.dart';
import 'package:nut_player/src/provider/json_provider/model/json_subtitles.dart';
import 'package:nut_player/src/provider/json_provider/parsing/response.dart';
class JsonProvider extends Provider {
final String urlPath;
JsonProvider(this.urlPath);
@override
Future<PlayerContent> retrieveContent() async {
final response = await http.get(Uri.parse(urlPath));
if (response.statusCode == 200) {
return _handleSucceedResponse(response);
} else {
throw JsonBadStatusCodeError();
}
}
JsonContent _handleSucceedResponse(http.Response httpResponse) {
var response = Response.fromJson(jsonDecode(httpResponse.body) as Map<String, dynamic>);
if (response.playbacks.isEmpty) { throw JsonNoPlaybacksError(); }
var uri = Uri.tryParse(response.playbacks.first.streamUrl);
if (uri != null) {
final statistics = response.statistics.map((stat) =>
JsonStatistics(
stat.name,
stat.urlTemplate,
stat.start,
stat.delay,
stat.count,
stat.method == "get" ? HTTPMethod.get : HTTPMethod.post,
stat.body
)
);
final subtitles = response.subtitles.map((sub) =>
JsonSubtitles(
sub.title,
sub.type == "srt" ? SubtitleType.srt : SubtitleType.unknown,
sub.url,
sub.language
)
);
final ContentType? contentType;
final urlPath = uri.toString();
if (urlPath.endsWith('.mp4')) {
contentType = Mp4ContentType(urlPath: urlPath);
} else if (urlPath.endsWith('.m3u8')) {
final isLive = response.playbacks.first.isLive;
contentType = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
contentType = null;
}
if (contentType != null) {
return JsonContent(
contentType,
statistics.toList(),
subtitles.toList()
);
} else {
throw JsonUnknownFormatError();
}
} else {
throw JsonIncorrectUrlError();
}
}
}
@@ -0,0 +1,14 @@
import 'package:nut_player/nut_player.dart';
class JsonContent extends PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
JsonContent(this.content, this.statistics, this.subtitles);
}
@@ -0,0 +1,5 @@
class JsonBadStatusCodeError extends Error {}
class JsonNoPlaybacksError extends Error {}
class JsonIncorrectUrlError extends Error {}
class JsonUnknownFormatError extends Error {}
@@ -0,0 +1,26 @@
import 'package:nut_player/nut_player.dart';
class JsonStatistics extends PlayerStatisticRecord {
@override
String name;
@override
String urlTemplate;
@override
double start;
@override
double delay;
@override
int count;
@override
HTTPMethod method;
@override
String? body;
JsonStatistics(this.name, this.urlTemplate, this.start, this.delay, this.count, this.method, this.body);
}
@@ -0,0 +1,17 @@
import 'package:nut_player/nut_player.dart';
class JsonSubtitles extends PlayerSubtitleRecord {
@override
String title;
@override
SubtitleType type;
@override
String url;
@override
String language;
JsonSubtitles(this.title, this.type, this.url, this.language);
}
@@ -0,0 +1,19 @@
class PlaybackRecord {
final String streamType;
final String streamUrl;
final bool isLive;
final bool isVideo;
final bool isAudio;
PlaybackRecord({required this.streamType, required this.streamUrl, required this.isLive, required this.isVideo, required this.isAudio});
factory PlaybackRecord.fromJson(dynamic data) {
return PlaybackRecord(
streamType: data['stream_type'],
streamUrl: data['stream_url'],
isLive: data['is_live'],
isVideo: data['is_video'],
isAudio: data['is_audio']
);
}
}
@@ -0,0 +1,23 @@
import 'statistic_record.dart';
import 'subtitle_record.dart';
import 'playback_record.dart';
class Response {
final List<PlaybackRecord> playbacks;
final List<StatisticRecord> statistics;
final List<PlayerSubtitleRecord> subtitles;
Response({required this.playbacks, required this.statistics, required this.subtitles});
factory Response.fromJson(dynamic data) {
var playbacks = data['playback'].map((item) => PlaybackRecord.fromJson(item)).toList();
var statistics = data['stat'].map((item) => StatisticRecord.fromJson(item)).toList();
var subtitles = data['subtitle'].map((item) => PlayerSubtitleRecord.fromJson(item)).toList();
return Response(
playbacks: List<PlaybackRecord>.from(playbacks),
statistics: List<StatisticRecord>.from(statistics),
subtitles: List<PlayerSubtitleRecord>.from(subtitles)
);
}
}
@@ -0,0 +1,23 @@
class StatisticRecord {
final String name;
final String urlTemplate;
final double start;
final double delay;
final int count;
final String method;
final String? body;
StatisticRecord({required this.name, required this.urlTemplate, required this.start, required this.delay, required this.count, required this.method, this.body});
factory StatisticRecord.fromJson(dynamic data) {
return StatisticRecord(
name: data['name'],
urlTemplate: data['url_template'],
start: data['start'] == null ? 0.0 : data['start'].toDouble(),
delay: data['delay'] == null ? 0.0 : data['delay'].toDouble(),
count: data['count'],
method: data['method'],
body: data['body'],
);
}
}

Some files were not shown because too many files have changed in this diff Show More