From 1276728a3524b77b1fd722736f906fc6429cab91 Mon Sep 17 00:00:00 2001 From: BGPark Date: Wed, 13 Mar 2024 23:25:28 +0900 Subject: [PATCH 1/5] change deprecated widgets and fix adult-vod options in channel vod list --- .../widgets/naver_login_headless_webview.dart | 9 +- .../vod/controller/vod_controller.dart | 11 +- .../vod/repository/vod_repository.dart | 7 +- lib/src/utils/focus/dpad_widget.dart | 10 +- .../virtual_keyboard_input_controller.dart | 19 ++- .../virtual_keyboard_layout.dart | 12 +- pubspec.lock | 148 ++++++++++-------- pubspec.yaml | 6 +- 8 files changed, 128 insertions(+), 94 deletions(-) diff --git a/lib/src/features/auth/widgets/naver_login_headless_webview.dart b/lib/src/features/auth/widgets/naver_login_headless_webview.dart index e7a0ff5..05ce301 100644 --- a/lib/src/features/auth/widgets/naver_login_headless_webview.dart +++ b/lib/src/features/auth/widgets/naver_login_headless_webview.dart @@ -61,11 +61,10 @@ class _NaverLoginHeadlessWebViewState ); if (isLoggedIn) { - if (context.mounted) { - ref - .read(dashboardControllerProvider.notifier) - .changeScreen(context, AppRoute.home); - } + if (!mounted) return; + ref + .read(dashboardControllerProvider.notifier) + .changeScreen(context, AppRoute.home); } else { // Check Error final errorMsg = await controller.evaluateJavascript(source: ''' diff --git a/lib/src/features/vod/controller/vod_controller.dart b/lib/src/features/vod/controller/vod_controller.dart index 467f2c6..5d50362 100644 --- a/lib/src/features/vod/controller/vod_controller.dart +++ b/lib/src/features/vod/controller/vod_controller.dart @@ -67,9 +67,14 @@ FutureOr channelVodTotalPages( ChannelVodTotalPagesRef ref, { required String channelId, }) async { - return await ref - .watch(vodRepositoryProvider) - .getChannelTotalPages(channelId: channelId); + final auth = await ref.watch(authControllerProvider.future); + + final options = auth?.getOptions(); + + return await ref.watch(vodRepositoryProvider).getChannelTotalPages( + channelId: channelId, + options: options, + ); } @riverpod diff --git a/lib/src/features/vod/repository/vod_repository.dart b/lib/src/features/vod/repository/vod_repository.dart index b1082d4..fea0801 100644 --- a/lib/src/features/vod/repository/vod_repository.dart +++ b/lib/src/features/vod/repository/vod_repository.dart @@ -28,11 +28,14 @@ class VodRepository { ), ); - Future getChannelTotalPages({required String channelId}) async { + Future getChannelTotalPages({ + required String channelId, + required Options? options, + }) async { final url = APIUrl.vodList(channelId); // Don't need auth options - final response = await _dio.get(url); + final response = await _dio.get(url, options: options); return response.data['content']['totalPages']; } diff --git a/lib/src/utils/focus/dpad_widget.dart b/lib/src/utils/focus/dpad_widget.dart index c3302c3..ee083d7 100644 --- a/lib/src/utils/focus/dpad_widget.dart +++ b/lib/src/utils/focus/dpad_widget.dart @@ -49,12 +49,12 @@ class DpadWidget extends HookWidget { return null; }, [widgetFocusNode]); - return RawKeyboardListener( + return KeyboardListener( autofocus: autofocus, focusNode: widgetFocusNode, - onKey: (event) { - if ((!useKeyUpEvent && event is RawKeyDownEvent) || - (useKeyUpEvent && event is RawKeyUpEvent)) { + onKeyEvent: (event) { + if ((!useKeyUpEvent && event is KeyDownEvent) || + (useKeyUpEvent && event is KeyUpEvent)) { onkeyEvent(context, event); } }, @@ -76,7 +76,7 @@ class DpadWidget extends HookWidget { ); } - void onkeyEvent(BuildContext context, RawKeyEvent event) { + void onkeyEvent(BuildContext context, KeyEvent event) { final String keyLabel = event.logicalKey.keyLabel; switch (keyLabel) { diff --git a/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.dart b/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.dart index e4a527e..97b4648 100644 --- a/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.dart +++ b/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.dart @@ -1,6 +1,8 @@ import 'package:hangul/hangul.dart' as hangul; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../virtual_keyboard_keys.dart'; + part 'virtual_keyboard_input_controller.g.dart'; @riverpod @@ -10,7 +12,18 @@ class VirtualKeyboardInputController extends _$VirtualKeyboardInputController { return ''; } - void addKoreanCharacter(String char) { + void addCharacter(String char, KeyboardLanguage language) { + // ENG + if (language == KeyboardLanguage.english) { + state = state + char; + } + // KOR + else { + _addKoreanCharacter(char); + } + } + + void _addKoreanCharacter(String char) { final input = hangul.HangulInput(state); input.pushCharacter(char); @@ -18,10 +31,6 @@ class VirtualKeyboardInputController extends _$VirtualKeyboardInputController { state = input.text; } - void addCharacter(String char) { - state = state + char; - } - void removeCharacter() { if (state.isNotEmpty) { state = state.substring(0, state.length - 1); diff --git a/lib/src/utils/virtual_keyboard/virtual_keyboard_layout.dart b/lib/src/utils/virtual_keyboard/virtual_keyboard_layout.dart index 25fb4bc..7488ea1 100644 --- a/lib/src/utils/virtual_keyboard/virtual_keyboard_layout.dart +++ b/lib/src/utils/virtual_keyboard/virtual_keyboard_layout.dart @@ -73,15 +73,9 @@ class VirtualKeyboardLayout extends HookConsumerWidget { : KeyboardLanguage.english; default: - if (keyboardLanguage.value == KeyboardLanguage.korean) { - ref - .read(virtualKeyboardInputControllerProvider.notifier) - .addKoreanCharacter(keyboardKey); - } else { - ref - .read(virtualKeyboardInputControllerProvider.notifier) - .addCharacter(keyboardKey); - } + ref + .read(virtualKeyboardInputControllerProvider.notifier) + .addCharacter(keyboardKey, keyboardLanguage.value); break; } diff --git a/pubspec.lock b/pubspec.lock index 5f93d0d..3e3689b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.1" cached_network_image: dependency: "direct main" description: @@ -237,34 +237,34 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217 + sha256: "445242371d91d2e24bd7b82e3583a2c05610094ba2d0575262484ad889c8f981" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.2" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "9cdd9987feaa6925ec5f98d64de4fbbb5d94248ff77bbf2489366efad6c4baef" + sha256: "4c0aed2a3491096e91cf1281923ba1b6814993f16dde0fd60f697925225bbbd6" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.2" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "9003a91409c9f1db6e2e50b4870d1d5e802e5923b25f7261bf3cb3e11ea9d4fb" + sha256: ce5d6215f4e143f7780ce53f73dfa6fc503f39d2d30bef76c48be9ac1a09d9a6 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.2" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" dbus: dependency: transitive description: @@ -277,10 +277,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" fake_async: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -410,10 +410,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" flutter_test: dependency: "direct dev" description: flutter @@ -484,10 +484,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" hotreloader: dependency: transitive description: @@ -508,10 +508,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -532,10 +532,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -568,6 +568,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -588,26 +612,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -644,10 +668,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -660,10 +684,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -780,10 +804,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" riverpod_analyzer_utils: dependency: transitive description: @@ -796,26 +820,26 @@ packages: dependency: "direct main" description: name: riverpod_annotation - sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95" + sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32" + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 url: "https://pub.dev" source: hosted - version: "2.3.11" + version: "2.4.0" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937 + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" rxdart: dependency: transitive description: @@ -868,10 +892,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -1049,18 +1073,18 @@ packages: dependency: "direct main" description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23 url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.8.3" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" video_player_avfoundation: dependency: transitive description: @@ -1081,18 +1105,18 @@ packages: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.0" vm_service: dependency: transitive description: name: vm_service - sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "14.0.0" + version: "13.0.0" wakelock_plus: dependency: "direct main" description: @@ -1121,26 +1145,26 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.1" web_socket_channel: dependency: "direct main" description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" xdg_directories: dependency: transitive description: @@ -1166,5 +1190,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.6 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 489b8af..181c1c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,13 +20,13 @@ dependencies: wakelock_plus: ^1.1.4 dio: ^5.4.0 flutter_inappwebview: ^6.0.0 - intl: ^0.18.1 + intl: ^0.19.0 web_socket_channel: ^2.4.0 shared_preferences: ^2.2.2 cached_network_image: ^3.3.1 flutter_hls_parser: ^2.0.1 - video_player: ^2.8.1 - chewie: ^1.7.4 + video_player: ^2.8.3 + chewie: ^1.7.5 hangul: ^0.6.0 dev_dependencies: From b7be507d834de07bc086b9277b9711f906bdc14f Mon Sep 17 00:00:00 2001 From: BGPark Date: Wed, 20 Mar 2024 01:50:15 +0900 Subject: [PATCH 2/5] v0.2.2 --- README.md | 13 ++++++++++--- lib/src/common/constants/app_version.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4e41965..3741c8c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,14 @@ ![동영상다시보기](./images/02.png) ![라이브탐색](./images/05.png) -## 핫픽스 - v0.2.1 +## 업데이트 +### v0.2.2 +- 채널 동영상에서 성인 인증 동영상이 재생되지 않는 현상 수정 + +### v0.2.1 - HeadlessWebView로 로그인이 진행되지 않는 분들을 위해 WebView 로그인을 추가했습니다. 가상 키보드에 문제가 있으신 분도 WebView 로그인을 사용해주세요. (설정 -> WebView 로그인) -## 패치노트 - v0.2.0 +## 패치노트 - v0.2 ### 1. 카테고리 검색, 라이브, 동영상, 홈 화면 즐겨찾기(로컬) - 홈 화면 즐겨찾기는 로컬 데이터로 저장되기 때문에 캐시를 삭제하면 초기화됩니다. ### 2. 동영상 전체 다시보기 추가 @@ -29,6 +33,9 @@ ## 개발자의 말 - 로그인 버튼 클릭 후 동작이 없으면 **모바일 네이버 앱 알림**을 확인해주세요(2단계 인증). 그래도 진행되지 않는다면 아이디 비밀번호를 다시 확인해주세요. (취소 후 다시 시도) - 텍스트 입력 시 리모컨 뒤로가기 버튼 대신, 가상키보드의 엔터 버튼으로 입력을 부탁드립니다. +- 유플러스 셋톱박스(또는 다른 TV)에서 리모컨 확인버튼이 클릭되지 않는 경우에 모바일 리모컨 어플리케이션을 통해 사용해주세요. + + [**U+tv리모컨앱 정보**](https://www.lguplus.com/iptv/main-feature/000PPT0036), [**U+tv리모컨앱 iOS**](https://apps.apple.com/kr/app/u-tv-%EB%A6%AC%EB%AA%A8%EC%BB%A8%EC%95%B1/id1637815745), [**U+tv리모컨앱 Android**](https://play.google.com/store/apps/details?id=com.lguplus.remocon&hl=ko&gl=US) ![로그인](./images/06.png) @@ -52,7 +59,7 @@ APK 파일을 다운받아서 수동으로 설치합니다. ``` ## 다운로드 -[**APK 파일 다운로드 (v0.2.1)**](https://github.com/Escaper-Park/unofficial_chzzk_android_tv/releases/tag/v0.2.1) +[**APK 파일 다운로드 (v0.2.2)**](https://github.com/Escaper-Park/unofficial_chzzk_android_tv/releases/tag/v0.2.2) ### 설치 파일 - 사용하시는 CPU 타입에 따라 설치하시면 됩니다. diff --git a/lib/src/common/constants/app_version.dart b/lib/src/common/constants/app_version.dart index 8f6a2dd..9842a24 100644 --- a/lib/src/common/constants/app_version.dart +++ b/lib/src/common/constants/app_version.dart @@ -1,3 +1,3 @@ class AppVersion { - static const String version = 'v0.2.1'; + static const String version = 'v0.2.2'; } diff --git a/pubspec.yaml b/pubspec.yaml index 181c1c3..a2bc23c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: unofficial_chzzk_android_tv description: "A Chzzk TV for Android TV" publish_to: "none" -version: 0.2.1 +version: 0.2.2 environment: sdk: ">=3.2.6 <4.0.0" From 774dd4e5501b713306a2bd918cba1af4c6a58cbd Mon Sep 17 00:00:00 2001 From: BGPark Date: Wed, 20 Mar 2024 03:22:11 +0900 Subject: [PATCH 3/5] fix age restricted vod error in channel and category-vod screen --- .../category/controller/category_vod_controller.dart | 8 ++++++++ .../category/widgets/streaming/category_vod_list.dart | 7 +++++++ .../features/channel/controller/channel_controller.dart | 7 +++++++ lib/src/features/channel/widgets/channel_vod_list.dart | 7 +++++++ lib/src/features/vod/controller/vod_controller.g.dart | 2 +- lib/src/features/vod/widgets/vod_container.dart | 8 +++----- lib/src/features/vod/widgets/vod_list.dart | 9 ++++++++- .../controller/virtual_keyboard_input_controller.g.dart | 2 +- 8 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/src/features/category/controller/category_vod_controller.dart b/lib/src/features/category/controller/category_vod_controller.dart index 3b14c04..0c7c33f 100644 --- a/lib/src/features/category/controller/category_vod_controller.dart +++ b/lib/src/features/category/controller/category_vod_controller.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:unofficial_chzzk_android_tv/src/features/vod/repository/vod_repository.dart'; import '../../auth/controller/auth_controller.dart'; import '../../vod/model/vod.dart'; @@ -57,4 +58,11 @@ class CategoryVodController extends _$CategoryVodController { }); } } + + Future getVodPath({required int videoNo}) async { + return await ref.watch(vodRepositoryProvider).getVodPath( + videoNo: videoNo, + options: _options, + ); + } } diff --git a/lib/src/features/category/widgets/streaming/category_vod_list.dart b/lib/src/features/category/widgets/streaming/category_vod_list.dart index f38ff08..6763033 100644 --- a/lib/src/features/category/widgets/streaming/category_vod_list.dart +++ b/lib/src/features/category/widgets/streaming/category_vod_list.dart @@ -62,6 +62,13 @@ class CategoryVodList extends HookConsumerWidget { VodContainer( autofocus: index == 0 ? true : false, vod: vod, + getVodPath: () async { + return await ref + .read(categoryVodControllerProvider( + category: category, + ).notifier) + .getVodPath(videoNo: vod.videoNo); + }, ), CategoryVodChannelInfo(channel: vod.channel), ], diff --git a/lib/src/features/channel/controller/channel_controller.dart b/lib/src/features/channel/controller/channel_controller.dart index 0068903..ffc8aa0 100644 --- a/lib/src/features/channel/controller/channel_controller.dart +++ b/lib/src/features/channel/controller/channel_controller.dart @@ -85,4 +85,11 @@ class ChannelVodController extends _$ChannelVodController { options: _options, ); } + + Future getVodPath({required int videoNo}) async { + return await ref.watch(vodRepositoryProvider).getVodPath( + videoNo: videoNo, + options: _options, + ); + } } diff --git a/lib/src/features/channel/widgets/channel_vod_list.dart b/lib/src/features/channel/widgets/channel_vod_list.dart index a978833..ba3cd60 100644 --- a/lib/src/features/channel/widgets/channel_vod_list.dart +++ b/lib/src/features/channel/widgets/channel_vod_list.dart @@ -53,6 +53,13 @@ class ChannelVodList extends ConsumerWidget { return VodContainer( autofocus: index == 0 ? true : false, vod: vod, + getVodPath: () async { + return await ref + .read(channelVodControllerProvider( + channelId: vod.channel.channelId) + .notifier) + .getVodPath(videoNo: vod.videoNo); + }, ); }, ), diff --git a/lib/src/features/vod/controller/vod_controller.g.dart b/lib/src/features/vod/controller/vod_controller.g.dart index 3a8f9e3..cedb6e0 100644 --- a/lib/src/features/vod/controller/vod_controller.g.dart +++ b/lib/src/features/vod/controller/vod_controller.g.dart @@ -7,7 +7,7 @@ part of 'vod_controller.dart'; // ************************************************************************** String _$channelVodTotalPagesHash() => - r'b24d46c85b3c79b2232e7dc2945b51134097a326'; + r'4168fb92621877dcd2de28fbb87f7e82f4dedaf6'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/src/features/vod/widgets/vod_container.dart b/lib/src/features/vod/widgets/vod_container.dart index de58aae..92ab417 100644 --- a/lib/src/features/vod/widgets/vod_container.dart +++ b/lib/src/features/vod/widgets/vod_container.dart @@ -7,7 +7,6 @@ import '../../../common/widgets/focused_outline_button.dart'; import '../../../common/widgets/rounded_container.dart'; import '../../../utils/popup/popup_utils.dart'; import '../../../utils/router/app_router.dart'; -import '../controller/vod_controller.dart'; import '../model/vod.dart'; import 'vod_thumbnail.dart'; import 'vod_info.dart'; @@ -16,11 +15,13 @@ class VodContainer extends ConsumerWidget { const VodContainer({ super.key, required this.vod, + required this.getVodPath, this.autofocus = false, }); final Vod vod; final bool autofocus; + final Future Function() getVodPath; @override Widget build(BuildContext context, WidgetRef ref) { @@ -33,10 +34,7 @@ class VodContainer extends ConsumerWidget { child: FocusedOutlineButton( autofocus: autofocus, onPressed: () async { - final String? vodPath = await ref - .read(vodControllerProvider(channelId: vod.channel.channelId) - .notifier) - .getVodPath(videoNo: vod.videoNo); + final String? vodPath = await getVodPath(); if (context.mounted) { if (vodPath == null) { diff --git a/lib/src/features/vod/widgets/vod_list.dart b/lib/src/features/vod/widgets/vod_list.dart index bc7dfc6..87cffb8 100644 --- a/lib/src/features/vod/widgets/vod_list.dart +++ b/lib/src/features/vod/widgets/vod_list.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/constants/dimensions.dart'; import '../../../common/widgets/center_text.dart'; -import 'vod_container.dart'; +import './vod_container.dart'; import '../controller/vod_controller.dart'; class VodList extends ConsumerWidget { @@ -40,6 +40,13 @@ class VodList extends ConsumerWidget { return VodContainer( autofocus: index == 0 ? true : false, vod: vod, + getVodPath: () async { + return await ref + .read(vodControllerProvider( + channelId: vod.channel.channelId) + .notifier) + .getVodPath(videoNo: vod.videoNo); + }, ); }, ), diff --git a/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.g.dart b/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.g.dart index 1516cf7..08fa736 100644 --- a/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.g.dart +++ b/lib/src/utils/virtual_keyboard/controller/virtual_keyboard_input_controller.g.dart @@ -7,7 +7,7 @@ part of 'virtual_keyboard_input_controller.dart'; // ************************************************************************** String _$virtualKeyboardInputControllerHash() => - r'ce0a13ff5ecc2f2728767edc4158b3d6a78d8ba8'; + r'6b8c8264933a55d87e4908442879219ae2bff056'; /// See also [VirtualKeyboardInputController]. @ProviderFor(VirtualKeyboardInputController) From 4ecea611d7aaf2490c0c1be657146dffbac8b454 Mon Sep 17 00:00:00 2001 From: BGPark Date: Wed, 20 Mar 2024 16:08:32 +0900 Subject: [PATCH 4/5] fix: gif emoji chat, chat msg error --- README.md | 1 + lib/src/common/widgets/optimized_image.dart | 57 +++++++++++++++++++ .../features/live/widgets/live_thumbnail.dart | 2 +- .../widgets/chat_container.dart | 33 +++++++++-- pubspec.lock | 8 +++ pubspec.yaml | 1 + 6 files changed, 96 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3741c8c..a6cbadd 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ ## 업데이트 ### v0.2.2 - 채널 동영상에서 성인 인증 동영상이 재생되지 않는 현상 수정 +- gif 이모티콘 대응, 채팅이 가끔 나오지 않는 현상 수정 ### v0.2.1 - HeadlessWebView로 로그인이 진행되지 않는 분들을 위해 WebView 로그인을 추가했습니다. 가상 키보드에 문제가 있으신 분도 WebView 로그인을 사용해주세요. (설정 -> WebView 로그인) diff --git a/lib/src/common/widgets/optimized_image.dart b/lib/src/common/widgets/optimized_image.dart index 01a07eb..67883cf 100644 --- a/lib/src/common/widgets/optimized_image.dart +++ b/lib/src/common/widgets/optimized_image.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:gif/gif.dart'; import '../constants/styles.dart'; @@ -125,3 +126,59 @@ class OptimizedAssetImage extends StatelessWidget { ); } } + +class OptimizedGifImage extends StatefulWidget { + const OptimizedGifImage({ + super.key, + required this.imageUrl, + required this.imageWidth, + this.imageHeight, + this.fit = BoxFit.cover, + }); + + final String imageUrl; + final double imageWidth; + final double? imageHeight; + final BoxFit fit; + + @override + State createState() => _OptimizedGifImageState(); +} + +class _OptimizedGifImageState extends State + with TickerProviderStateMixin { + late final GifController controller; + final int _fps = 30; + + @override + void initState() { + controller = GifController(vsync: this); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Gif( + controller: controller, + fps: _fps, + autostart: Autostart.once, + useCache: true, + height: widget.imageHeight, + width: widget.imageWidth, + image: NetworkImage( + widget.imageUrl, + ), + fit: widget.fit, + placeholder: (context) => SizedBox( + height: widget.imageHeight, + width: widget.imageWidth, + ), + ); + } +} diff --git a/lib/src/features/live/widgets/live_thumbnail.dart b/lib/src/features/live/widgets/live_thumbnail.dart index f2d5565..687f72a 100644 --- a/lib/src/features/live/widgets/live_thumbnail.dart +++ b/lib/src/features/live/widgets/live_thumbnail.dart @@ -123,7 +123,7 @@ class _LiveThumbnailImage extends StatelessWidget { imageWidth: liveThumbnailWidth, imageHeight: liveThumbnailHeight, ), - if (liveDetail.userAdultStatus == 'ADULT') + if (liveDetail.userAdultStatus == 'ADULT' || liveDetail.adult) OptimizedAssetImage( imagePath: AssetsPath.ageRestrictionTransprent, imageWidth: liveThumbnailWidth, diff --git a/lib/src/features/live_streaming/widgets/chat_container.dart b/lib/src/features/live_streaming/widgets/chat_container.dart index 2fb025b..de5660b 100644 --- a/lib/src/features/live_streaming/widgets/chat_container.dart +++ b/lib/src/features/live_streaming/widgets/chat_container.dart @@ -21,18 +21,40 @@ class ChatContainer extends StatelessWidget { final Color nicknameColor = getNicknameColor(chat.nickname); List textSpans = []; + if (chat.emojis == null) { + textSpans.add( + WidgetSpan( + child: Text( + chat.msg, + style: TextStyle( + fontSize: fontSize, + ), + ), + ), + ); + } + if (chat.emojis != null) { chat.msg.splitMapJoin( RegExp(r'\{:([^}]+):\}'), onMatch: (match) { final String? emojiKey = match.group(1); + // Emoji if (chat.emojis!.containsKey(emojiKey)) { + final imageUrl = chat.emojis![emojiKey]!; + textSpans.add(WidgetSpan( - child: OptimizedNetworkImage( - imageUrl: chat.emojis![emojiKey]!, - imageWidth: fontSize + 5.0, - imageHeight: fontSize + 5.0, - ), + child: imageUrl.contains('.gif') + ? OptimizedGifImage( + imageUrl: imageUrl, + imageWidth: fontSize + 5.0, + imageHeight: fontSize + 5.0, + ) + : OptimizedNetworkImage( + imageUrl: imageUrl, + imageWidth: fontSize + 5.0, + imageHeight: fontSize + 5.0, + ), )); } else { textSpans.add( @@ -46,6 +68,7 @@ class ChatContainer extends StatelessWidget { } return ''; }, + // Chat onNonMatch: (nonMatch) { textSpans.add( TextSpan( diff --git a/pubspec.lock b/pubspec.lock index 3e3689b..7fbab14 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -448,6 +448,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + gif: + dependency: "direct main" + description: + name: gif + sha256: ade95694f1471da737922806818ffade2814d1d7f8d10af38ebcf36ace012bc0 + url: "https://pub.dev" + source: hosted + version: "2.3.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a2bc23c..6c1b342 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: video_player: ^2.8.3 chewie: ^1.7.5 hangul: ^0.6.0 + gif: ^2.3.0 dev_dependencies: flutter_test: From f3a1b7593fde572ee4c1eccea9324c20dc7a32ec Mon Sep 17 00:00:00 2001 From: BGPark Date: Sat, 23 Mar 2024 20:22:41 +0900 Subject: [PATCH 5/5] v0.2.2 --- README.md | 11 +- lib/src/common/constants/api.dart | 15 +- lib/src/common/widgets/header_text.dart | 11 +- .../controller/category_vod_controller.g.dart | 2 +- .../controller/channel_controller.g.dart | 2 +- lib/src/features/channel/model/channel.dart | 19 - .../channel/model/channel.freezed.dart | 325 ----------------- lib/src/features/channel/model/channel.g.dart | 14 - .../repository/channel_repository.dart | 37 +- lib/src/features/home/home_screen.dart | 44 ++- .../widgets/home_favorite_categories.dart | 123 +------ .../home_favorite_category_container.dart | 81 +++++ .../home/widgets/home_following_lives.dart | 76 ++-- .../home/widgets/home_function_buttons.dart | 116 ++++++ .../home/widgets/home_popular_lives.dart | 65 ++-- .../home/widgets/home_refresh_button.dart | 46 --- lib/src/features/live/all_lives_screen.dart | 73 ++++ .../live/controller/live_controller.dart | 154 +++++++- .../live/controller/live_controller.g.dart | 73 +++- lib/src/features/live/model/all_lives.dart | 25 ++ .../live/model/all_lives.freezed.dart | 341 ++++++++++++++++++ lib/src/features/live/model/all_lives.g.dart | 21 ++ .../live/repository/live_repository.dart | 50 +++ .../widgets/all_lives/all_lives_list.dart | 63 ++++ .../all_lives/all_lives_sidebar_buttons.dart | 86 +++++ .../widgets/chat_container.dart | 10 +- .../controller/settings_controller.dart | 1 - .../controller/update_controller.dart | 27 ++ .../controller/update_controller.g.dart | 26 ++ .../repository/settings_repository.dart | 2 +- .../repository/update_repository.dart | 60 +++ .../repository/update_repository.g.dart | 25 ++ .../features/settings/settings_screen.dart | 87 ++++- lib/src/utils/focus/dpad_widget.dart | 3 + lib/src/utils/router/app_router.dart | 15 + lib/src/utils/router/app_router.g.dart | 2 +- lib/src/utils/router/screens.dart | 8 +- .../controller/live_stream_controller.dart | 2 +- .../controller/live_stream_controller.g.dart | 2 +- .../common/live_explore_error_button.dart | 32 ++ .../live_stream_category_live_list.dart | 69 ++-- .../following/live_stream_following_list.dart | 15 +- .../popular/live_stream_popular_list.dart | 26 +- pubspec.lock | 26 +- pubspec.yaml | 2 + 45 files changed, 1550 insertions(+), 763 deletions(-) create mode 100644 lib/src/features/home/widgets/home_favorite_category_container.dart create mode 100644 lib/src/features/home/widgets/home_function_buttons.dart delete mode 100644 lib/src/features/home/widgets/home_refresh_button.dart create mode 100644 lib/src/features/live/all_lives_screen.dart create mode 100644 lib/src/features/live/model/all_lives.dart create mode 100644 lib/src/features/live/model/all_lives.freezed.dart create mode 100644 lib/src/features/live/model/all_lives.g.dart create mode 100644 lib/src/features/live/widgets/all_lives/all_lives_list.dart create mode 100644 lib/src/features/live/widgets/all_lives/all_lives_sidebar_buttons.dart create mode 100644 lib/src/features/settings/controller/update_controller.dart create mode 100644 lib/src/features/settings/controller/update_controller.g.dart create mode 100644 lib/src/features/settings/repository/update_repository.dart create mode 100644 lib/src/features/settings/repository/update_repository.g.dart create mode 100644 lib/src/utils/video_player/widgets/common/live_explore_error_button.dart diff --git a/README.md b/README.md index a6cbadd..3e1af6b 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,10 @@ ## 업데이트 ### v0.2.2 -- 채널 동영상에서 성인 인증 동영상이 재생되지 않는 현상 수정 -- gif 이모티콘 대응, 채팅이 가끔 나오지 않는 현상 수정 +- 채널 동영상에서 연령 인증 동영상이 재생되지 않는 현상 수정 +- gif 이모티콘 성능 최적화, 채팅이 가끔 나오지 않는 현상 수정 +- 전체 라이브 페이지 추가 (홈 화면 상단 -> 전체 라이브) +- 기타 기능 및 버그 수정 ### v0.2.1 - HeadlessWebView로 로그인이 진행되지 않는 분들을 위해 WebView 로그인을 추가했습니다. 가상 키보드에 문제가 있으신 분도 WebView 로그인을 사용해주세요. (설정 -> WebView 로그인) @@ -88,7 +90,6 @@ APK 파일을 다운받아서 수동으로 설치합니다. ## Future Works - 화질 설정 (멀티뷰 성능 이슈) -- 멀티뷰 소리조절 +- 멀티뷰 최적화 - 팔로우 추가/제거 -- 삼성TV (Tizen OS) 지원 (오래걸림) - +- 삼성TV (Tizen OS) 지원 (오래걸림) \ No newline at end of file diff --git a/lib/src/common/constants/api.dart b/lib/src/common/constants/api.dart index 3002bd1..5613e9c 100644 --- a/lib/src/common/constants/api.dart +++ b/lib/src/common/constants/api.dart @@ -1,3 +1,5 @@ +import '../../features/live/repository/live_repository.dart'; + class APIUrl { static const String _chzzkAPIUrl = 'https://api.chzzk.naver.com'; static const String _naverGameUrl = @@ -47,15 +49,16 @@ class APIUrl { }) => '$_chzzkAPIUrl/service/v1/search/channels?keyword=$keyword&offset=$offset&size=$size&withFirstChannelContent=$withFirstChannelContent'; - static String popularLive({ + static String allLives({ required int? concurrentUserCount, required int? liveId, - int size = 20, + int size = 18, + LiveSortType sortType = LiveSortType.popular, }) { if (concurrentUserCount == null || liveId == null) { - return '$_chzzkAPIUrl/service/v1/lives?size=$size&sortType=POPULAR'; + return '$_chzzkAPIUrl/service/v1/lives?size=$size&sortType=${sortType.sortType}'; } else { - return '$_chzzkAPIUrl/service/v1/lives?concurrentUserCount=$concurrentUserCount&liveId=$liveId&sortType=POPULAR'; + return '$_chzzkAPIUrl/service/v1/lives?concurrentUserCount=$concurrentUserCount&liveId=$liveId&sortType=${sortType.sortType}'; } } @@ -110,4 +113,8 @@ class APIUrl { static String chatServer(int serverNo) => 'wss://kr-ss$serverNo.chat.naver.com/chat'; + + // Github update check + static String latestApp() => + 'https://api.github.com/repos/Escaper-Park/unofficial_chzzk_android_tv/releases/latest'; } diff --git a/lib/src/common/widgets/header_text.dart b/lib/src/common/widgets/header_text.dart index f7510d5..d365926 100644 --- a/lib/src/common/widgets/header_text.dart +++ b/lib/src/common/widgets/header_text.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../constants/styles.dart'; -import 'focused_outline_button.dart'; +import './focused_outline_button.dart'; class HeaderText extends StatelessWidget { /// A subject title header in a screen. @@ -11,6 +11,7 @@ class HeaderText extends StatelessWidget { this.verticalPadding = 12.0, this.fontSize = 20.0, this.useShowMoreButton = false, + this.focusNode, this.onPressed, }); @@ -18,6 +19,7 @@ class HeaderText extends StatelessWidget { final double verticalPadding; final double fontSize; final bool useShowMoreButton; + final FocusNode? focusNode; final VoidCallback? onPressed; @override @@ -29,6 +31,7 @@ class HeaderText extends StatelessWidget { children: [ _headerText(), FocusedOutlineButton( + focusNode: focusNode, padding: const EdgeInsets.all(5.0), onPressed: onPressed, child: const Icon( @@ -43,7 +46,11 @@ class HeaderText extends StatelessWidget { Widget _headerText() { return Padding( - padding: EdgeInsets.symmetric(vertical: verticalPadding), + padding: EdgeInsets.only( + top: verticalPadding, + bottom: verticalPadding, + left: 5.0, + ), child: Text( text, style: TextStyle( diff --git a/lib/src/features/category/controller/category_vod_controller.g.dart b/lib/src/features/category/controller/category_vod_controller.g.dart index b30c08c..b5ac4b9 100644 --- a/lib/src/features/category/controller/category_vod_controller.g.dart +++ b/lib/src/features/category/controller/category_vod_controller.g.dart @@ -7,7 +7,7 @@ part of 'category_vod_controller.dart'; // ************************************************************************** String _$categoryVodControllerHash() => - r'828f6740390b22262ee1d16f755ec8cd23187deb'; + r'e05976664757a3ce8ba0932e32c6d833fadb7e1c'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/src/features/channel/controller/channel_controller.g.dart b/lib/src/features/channel/controller/channel_controller.g.dart index a9f22be..a2e27c5 100644 --- a/lib/src/features/channel/controller/channel_controller.g.dart +++ b/lib/src/features/channel/controller/channel_controller.g.dart @@ -192,7 +192,7 @@ class _ChannelLiveControllerProviderElement } String _$channelVodControllerHash() => - r'd3cae44e11cd006b9644a45c3bb5a3654b114935'; + r'c19867db02352d0507a9826a7c2c3152a3112a2e'; abstract class _$ChannelVodController extends BuildlessAutoDisposeAsyncNotifier?> { diff --git a/lib/src/features/channel/model/channel.dart b/lib/src/features/channel/model/channel.dart index a42cf5f..2a54b3d 100644 --- a/lib/src/features/channel/model/channel.dart +++ b/lib/src/features/channel/model/channel.dart @@ -29,22 +29,3 @@ class PersonalData with _$PersonalData { factory PersonalData.fromJson(Map json) => _$PersonalDataFromJson(json); } - -@freezed -class PopularChannelPage with _$PopularChannelPage { - factory PopularChannelPage({ - required int? concurrentUserCount, - required int? liveId, - }) = _PopularChannelPage; - - factory PopularChannelPage.fromJson(Map json) => - _$PopularChannelPageFromJson(json); -} - -@freezed -class PopularChannelResponse with _$PopularChannelResponse { - const factory PopularChannelResponse({ - required List? channels, - required PopularChannelPage? page, - }) = _PopularChannelResponse; -} diff --git a/lib/src/features/channel/model/channel.freezed.dart b/lib/src/features/channel/model/channel.freezed.dart index 8e011d7..169f93e 100644 --- a/lib/src/features/channel/model/channel.freezed.dart +++ b/lib/src/features/channel/model/channel.freezed.dart @@ -461,328 +461,3 @@ abstract class _PersonalData implements PersonalData { _$$PersonalDataImplCopyWith<_$PersonalDataImpl> get copyWith => throw _privateConstructorUsedError; } - -PopularChannelPage _$PopularChannelPageFromJson(Map json) { - return _PopularChannelPage.fromJson(json); -} - -/// @nodoc -mixin _$PopularChannelPage { - int? get concurrentUserCount => throw _privateConstructorUsedError; - int? get liveId => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $PopularChannelPageCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $PopularChannelPageCopyWith<$Res> { - factory $PopularChannelPageCopyWith( - PopularChannelPage value, $Res Function(PopularChannelPage) then) = - _$PopularChannelPageCopyWithImpl<$Res, PopularChannelPage>; - @useResult - $Res call({int? concurrentUserCount, int? liveId}); -} - -/// @nodoc -class _$PopularChannelPageCopyWithImpl<$Res, $Val extends PopularChannelPage> - implements $PopularChannelPageCopyWith<$Res> { - _$PopularChannelPageCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? concurrentUserCount = freezed, - Object? liveId = freezed, - }) { - return _then(_value.copyWith( - concurrentUserCount: freezed == concurrentUserCount - ? _value.concurrentUserCount - : concurrentUserCount // ignore: cast_nullable_to_non_nullable - as int?, - liveId: freezed == liveId - ? _value.liveId - : liveId // ignore: cast_nullable_to_non_nullable - as int?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$PopularChannelPageImplCopyWith<$Res> - implements $PopularChannelPageCopyWith<$Res> { - factory _$$PopularChannelPageImplCopyWith(_$PopularChannelPageImpl value, - $Res Function(_$PopularChannelPageImpl) then) = - __$$PopularChannelPageImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({int? concurrentUserCount, int? liveId}); -} - -/// @nodoc -class __$$PopularChannelPageImplCopyWithImpl<$Res> - extends _$PopularChannelPageCopyWithImpl<$Res, _$PopularChannelPageImpl> - implements _$$PopularChannelPageImplCopyWith<$Res> { - __$$PopularChannelPageImplCopyWithImpl(_$PopularChannelPageImpl _value, - $Res Function(_$PopularChannelPageImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? concurrentUserCount = freezed, - Object? liveId = freezed, - }) { - return _then(_$PopularChannelPageImpl( - concurrentUserCount: freezed == concurrentUserCount - ? _value.concurrentUserCount - : concurrentUserCount // ignore: cast_nullable_to_non_nullable - as int?, - liveId: freezed == liveId - ? _value.liveId - : liveId // ignore: cast_nullable_to_non_nullable - as int?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$PopularChannelPageImpl implements _PopularChannelPage { - _$PopularChannelPageImpl( - {required this.concurrentUserCount, required this.liveId}); - - factory _$PopularChannelPageImpl.fromJson(Map json) => - _$$PopularChannelPageImplFromJson(json); - - @override - final int? concurrentUserCount; - @override - final int? liveId; - - @override - String toString() { - return 'PopularChannelPage(concurrentUserCount: $concurrentUserCount, liveId: $liveId)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PopularChannelPageImpl && - (identical(other.concurrentUserCount, concurrentUserCount) || - other.concurrentUserCount == concurrentUserCount) && - (identical(other.liveId, liveId) || other.liveId == liveId)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, concurrentUserCount, liveId); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$PopularChannelPageImplCopyWith<_$PopularChannelPageImpl> get copyWith => - __$$PopularChannelPageImplCopyWithImpl<_$PopularChannelPageImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$PopularChannelPageImplToJson( - this, - ); - } -} - -abstract class _PopularChannelPage implements PopularChannelPage { - factory _PopularChannelPage( - {required final int? concurrentUserCount, - required final int? liveId}) = _$PopularChannelPageImpl; - - factory _PopularChannelPage.fromJson(Map json) = - _$PopularChannelPageImpl.fromJson; - - @override - int? get concurrentUserCount; - @override - int? get liveId; - @override - @JsonKey(ignore: true) - _$$PopularChannelPageImplCopyWith<_$PopularChannelPageImpl> get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -mixin _$PopularChannelResponse { - List? get channels => throw _privateConstructorUsedError; - PopularChannelPage? get page => throw _privateConstructorUsedError; - - @JsonKey(ignore: true) - $PopularChannelResponseCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $PopularChannelResponseCopyWith<$Res> { - factory $PopularChannelResponseCopyWith(PopularChannelResponse value, - $Res Function(PopularChannelResponse) then) = - _$PopularChannelResponseCopyWithImpl<$Res, PopularChannelResponse>; - @useResult - $Res call({List? channels, PopularChannelPage? page}); - - $PopularChannelPageCopyWith<$Res>? get page; -} - -/// @nodoc -class _$PopularChannelResponseCopyWithImpl<$Res, - $Val extends PopularChannelResponse> - implements $PopularChannelResponseCopyWith<$Res> { - _$PopularChannelResponseCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? channels = freezed, - Object? page = freezed, - }) { - return _then(_value.copyWith( - channels: freezed == channels - ? _value.channels - : channels // ignore: cast_nullable_to_non_nullable - as List?, - page: freezed == page - ? _value.page - : page // ignore: cast_nullable_to_non_nullable - as PopularChannelPage?, - ) as $Val); - } - - @override - @pragma('vm:prefer-inline') - $PopularChannelPageCopyWith<$Res>? get page { - if (_value.page == null) { - return null; - } - - return $PopularChannelPageCopyWith<$Res>(_value.page!, (value) { - return _then(_value.copyWith(page: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$PopularChannelResponseImplCopyWith<$Res> - implements $PopularChannelResponseCopyWith<$Res> { - factory _$$PopularChannelResponseImplCopyWith( - _$PopularChannelResponseImpl value, - $Res Function(_$PopularChannelResponseImpl) then) = - __$$PopularChannelResponseImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({List? channels, PopularChannelPage? page}); - - @override - $PopularChannelPageCopyWith<$Res>? get page; -} - -/// @nodoc -class __$$PopularChannelResponseImplCopyWithImpl<$Res> - extends _$PopularChannelResponseCopyWithImpl<$Res, - _$PopularChannelResponseImpl> - implements _$$PopularChannelResponseImplCopyWith<$Res> { - __$$PopularChannelResponseImplCopyWithImpl( - _$PopularChannelResponseImpl _value, - $Res Function(_$PopularChannelResponseImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? channels = freezed, - Object? page = freezed, - }) { - return _then(_$PopularChannelResponseImpl( - channels: freezed == channels - ? _value._channels - : channels // ignore: cast_nullable_to_non_nullable - as List?, - page: freezed == page - ? _value.page - : page // ignore: cast_nullable_to_non_nullable - as PopularChannelPage?, - )); - } -} - -/// @nodoc - -class _$PopularChannelResponseImpl implements _PopularChannelResponse { - const _$PopularChannelResponseImpl( - {required final List? channels, required this.page}) - : _channels = channels; - - final List? _channels; - @override - List? get channels { - final value = _channels; - if (value == null) return null; - if (_channels is EqualUnmodifiableListView) return _channels; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - - @override - final PopularChannelPage? page; - - @override - String toString() { - return 'PopularChannelResponse(channels: $channels, page: $page)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PopularChannelResponseImpl && - const DeepCollectionEquality().equals(other._channels, _channels) && - (identical(other.page, page) || other.page == page)); - } - - @override - int get hashCode => Object.hash( - runtimeType, const DeepCollectionEquality().hash(_channels), page); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$PopularChannelResponseImplCopyWith<_$PopularChannelResponseImpl> - get copyWith => __$$PopularChannelResponseImplCopyWithImpl< - _$PopularChannelResponseImpl>(this, _$identity); -} - -abstract class _PopularChannelResponse implements PopularChannelResponse { - const factory _PopularChannelResponse( - {required final List? channels, - required final PopularChannelPage? page}) = _$PopularChannelResponseImpl; - - @override - List? get channels; - @override - PopularChannelPage? get page; - @override - @JsonKey(ignore: true) - _$$PopularChannelResponseImplCopyWith<_$PopularChannelResponseImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/src/features/channel/model/channel.g.dart b/lib/src/features/channel/model/channel.g.dart index bb7ff41..1837509 100644 --- a/lib/src/features/channel/model/channel.g.dart +++ b/lib/src/features/channel/model/channel.g.dart @@ -41,17 +41,3 @@ Map _$$PersonalDataImplToJson(_$PersonalDataImpl instance) => { 'privateUserBlock': instance.privateUserBlock, }; - -_$PopularChannelPageImpl _$$PopularChannelPageImplFromJson( - Map json) => - _$PopularChannelPageImpl( - concurrentUserCount: json['concurrentUserCount'] as int?, - liveId: json['liveId'] as int?, - ); - -Map _$$PopularChannelPageImplToJson( - _$PopularChannelPageImpl instance) => - { - 'concurrentUserCount': instance.concurrentUserCount, - 'liveId': instance.liveId, - }; diff --git a/lib/src/features/channel/repository/channel_repository.dart b/lib/src/features/channel/repository/channel_repository.dart index 5f78c05..86bdc4d 100644 --- a/lib/src/features/channel/repository/channel_repository.dart +++ b/lib/src/features/channel/repository/channel_repository.dart @@ -31,42 +31,7 @@ class ChannelRepository { return Channel.fromJson(response.data['content']); } - Future getPopularChannelResponse({ - required Options? options, - required int? concurrentUserCount, - required int? liveId, - int size = 20, - }) async { - final url = APIUrl.popularLive( - concurrentUserCount: concurrentUserCount, - liveId: liveId, - size: size, - ); - - final response = await _dio.get(url, options: options); - - final Map? pageResponse = response.data['content']['page']; - final List channelsResponse = response.data['content']['data']; - - final PopularChannelPage? page = pageResponse == null - ? null - : PopularChannelPage.fromJson(pageResponse['next']); - final List channels = channelsResponse - // remove blocked channel - .where((response) { - final personalData = response['channel']['personalData']; - if (personalData == null) return true; - - return personalData['privateUserBlock'] != true; - }) - .map((response) => Channel.fromJson(response['channel'])) - .toList(); - - return PopularChannelResponse( - channels: channels, - page: page, - ); - } + Future?> getRecommendChannels({ required Options? options, diff --git a/lib/src/features/home/home_screen.dart b/lib/src/features/home/home_screen.dart index 9565ed8..51a4ff8 100644 --- a/lib/src/features/home/home_screen.dart +++ b/lib/src/features/home/home_screen.dart @@ -1,30 +1,44 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../common/widgets/center_text.dart'; +import '../../common/widgets/header_text.dart'; +import '../auth/controller/auth_controller.dart'; import '../dashboard/dashboard_screen.dart'; import './widgets/home_following_lives.dart'; import './widgets/home_popular_lives.dart'; -import 'widgets/home_favorite_categories.dart'; -import './widgets/home_refresh_button.dart'; +import './widgets/home_favorite_categories.dart'; +import 'widgets/home_function_buttons.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final asyncAuth = ref.watch(authControllerProvider); + return DashboardScreen( screen: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: ListView( - children: const [ - HomeRefreshButton(), - // Following live channels - HomeFollowingLives(), - // Popular live channels - HomePopularLives(), - // Categories - HomeFavoriteCategories(), - ], - ), + child: switch (asyncAuth) { + AsyncData(:final value) => ListView( + children: [ + const HomeFunctionButtons(), + // Following live channels + if (value != null) const HeaderText(text: '팔로잉 라이브 채널'), + if (value != null) const HomeFollowingLives(), + // Popular live channels + const HeaderText(text: '인기 라이브 채널'), + const HomePopularLives(), + // Categories + const HeaderText(text: '즐겨찾는 카테고리'), + const HomeFavoriteCategories(), + ], + ), + AsyncError() => const CenterText(text: '로그인 에러'), + _ => const SizedBox.shrink(), + }, ), ); } diff --git a/lib/src/features/home/widgets/home_favorite_categories.dart b/lib/src/features/home/widgets/home_favorite_categories.dart index 00914b4..5967eba 100644 --- a/lib/src/features/home/widgets/home_favorite_categories.dart +++ b/lib/src/features/home/widgets/home_favorite_categories.dart @@ -1,19 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; -import '../../../common/constants/assets_path.dart'; import '../../../common/constants/dimensions.dart'; -import '../../../common/constants/styles.dart'; import '../../../common/widgets/center_text.dart'; -import '../../../common/widgets/focused_outline_button.dart'; -import '../../../common/widgets/header_text.dart'; -import '../../../common/widgets/optimized_image.dart'; -import '../../../common/widgets/rounded_container.dart'; -import '../../../utils/router/app_router.dart'; import '../../category/controller/category_controller.dart'; -import '../../category/model/category.dart'; -import '../../live/widgets/live_badge.dart'; +import './home_favorite_category_container.dart'; class HomeFavoriteCategories extends ConsumerWidget { const HomeFavoriteCategories({super.key}); @@ -23,103 +14,25 @@ class HomeFavoriteCategories extends ConsumerWidget { final asyncFavoriteCategories = ref.watch(favoriteCategoriesControllerProvider); - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const HeaderText( - text: '즐겨찾는 카테고리', - onPressed: null, - ), - SizedBox( - height: Dimensions.homeFavoriteCategorySize.height, - child: switch (asyncFavoriteCategories) { - AsyncData(:final value) => value.isEmpty - ? const CenterText(text: '즐겨찾기한 카테고리가 없습니다') - : ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: value.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: _HomeFavoriteCategoryContainer(value[index]), - ); - }, - ), - AsyncError() => const CenterText(text: '카테고리 즐겨찾기를 불러올 수 없습니다'), - _ => const CenterText(text: '즐겨찾기 카테고리 불러오는 중...'), - }, - ), - ], - ); - } -} - -class _HomeFavoriteCategoryContainer extends StatelessWidget { - const _HomeFavoriteCategoryContainer(this.category); - - final Category category; - - @override - Widget build(BuildContext context) { - final double imageWidth = Dimensions.homeFavoriteCategorySize.width; - final formatter = NumberFormat("#,###", 'en_US'); - - return RoundedContainer( - backgroundColor: AppColors.greyContainerColor, - borderRadius: 12.0, - width: imageWidth, + return SizedBox( height: Dimensions.homeFavoriteCategorySize.height, - child: FocusedOutlineButton( - onPressed: () { - context.goNamed( - AppRoute.categoryStreaming.routeName, - extra: { - 'category': category, - 'fromHome': true, - }, - ); - }, - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12.0), - child: category.posterImageUrl == null - ? OptimizedAssetImage( - imagePath: AssetsPath.categoryBaseThumbnail, - imageWidth: imageWidth, - ) - : OptimizedNetworkImage( - imageUrl: category.posterImageUrl!, - imageWidth: imageWidth, - ), - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Align( - alignment: Alignment.topLeft, - child: LiveBadge( - backgroundColor: - AppColors.greyContainerColor.withOpacity(0.65), - text: category.categoryValue, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Align( - alignment: Alignment.bottomRight, - child: LiveBadge( - text: - '${formatter.format(category.openLiveCount)} 라이브\n${formatter.format(category.concurrentUserCount)} 명', - backgroundColor: - AppColors.greyContainerColor.withOpacity(0.65), - ), + child: switch (asyncFavoriteCategories) { + AsyncData(:final value) => value.isEmpty + ? const CenterText(text: '즐겨찾기한 카테고리가 없습니다') + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: value.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: + HomeFavoriteCategoryContainer(category: value[index]), + ); + }, ), - ), - ], - ), - ), + AsyncError() => const CenterText(text: '카테고리 즐겨찾기를 불러올 수 없습니다'), + _ => const CenterText(text: '즐겨찾기 카테고리 불러오는 중...'), + }, ); } } diff --git a/lib/src/features/home/widgets/home_favorite_category_container.dart b/lib/src/features/home/widgets/home_favorite_category_container.dart new file mode 100644 index 0000000..dc8eb20 --- /dev/null +++ b/lib/src/features/home/widgets/home_favorite_category_container.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../common/constants/assets_path.dart'; +import '../../../common/constants/dimensions.dart'; +import '../../../common/constants/styles.dart'; +import '../../../common/widgets/focused_outline_button.dart'; +import '../../../common/widgets/optimized_image.dart'; +import '../../../common/widgets/rounded_container.dart'; +import '../../../utils/router/app_router.dart'; +import '../../category/model/category.dart'; +import '../../live/widgets/live_badge.dart'; + +class HomeFavoriteCategoryContainer extends StatelessWidget { + const HomeFavoriteCategoryContainer({super.key, required this.category}); + + final Category category; + + @override + Widget build(BuildContext context) { + final double imageWidth = Dimensions.homeFavoriteCategorySize.width; + final formatter = NumberFormat("#,###", 'en_US'); + + return RoundedContainer( + backgroundColor: AppColors.greyContainerColor, + borderRadius: 12.0, + width: imageWidth, + height: Dimensions.homeFavoriteCategorySize.height, + child: FocusedOutlineButton( + onPressed: () { + context.goNamed( + AppRoute.categoryStreaming.routeName, + extra: { + 'category': category, + 'fromHome': true, + }, + ); + }, + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: category.posterImageUrl == null + ? OptimizedAssetImage( + imagePath: AssetsPath.categoryBaseThumbnail, + imageWidth: imageWidth, + ) + : OptimizedNetworkImage( + imageUrl: category.posterImageUrl!, + imageWidth: imageWidth, + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.topLeft, + child: LiveBadge( + backgroundColor: + AppColors.greyContainerColor.withOpacity(0.65), + text: category.categoryValue, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.bottomRight, + child: LiveBadge( + text: + '${formatter.format(category.openLiveCount)} 라이브\n${formatter.format(category.concurrentUserCount)} 명', + backgroundColor: + AppColors.greyContainerColor.withOpacity(0.65), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/home/widgets/home_following_lives.dart b/lib/src/features/home/widgets/home_following_lives.dart index ffe808c..4c0a322 100644 --- a/lib/src/features/home/widgets/home_following_lives.dart +++ b/lib/src/features/home/widgets/home_following_lives.dart @@ -1,63 +1,47 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/widgets/center_text.dart'; -import '../../../common/widgets/header_text.dart'; import '../../live/controller/live_controller.dart'; import '../../live/widgets/live_container.dart'; import './home_base_container.dart'; -class HomeFollowingLives extends ConsumerWidget { +class HomeFollowingLives extends HookConsumerWidget { const HomeFollowingLives({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final asyncFollowingLives = ref.watch(followingLiveControllerProvider); + final scrollController = useScrollController(); - return switch (asyncFollowingLives) { - AsyncData(:final value) => value == null - ? const SizedBox.shrink() - : Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const HeaderText( - text: '팔로잉 라이브 채널', - useShowMoreButton: false, - ), - HomeBaseContainer( - child: value.isEmpty - ? const CenterText(text: '아무도 방송을 키지 않았어요') - : ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: value.length, - itemBuilder: (context, index) { - final liveDetail = value[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: HomeBaseContainer( + child: switch (asyncFollowingLives) { + AsyncData(:final value) => value == null + ? const CenterText(text: '팔로잉 채널을 불러오는데 실패했습니다') + : value.isEmpty + ? const CenterText(text: '아무도 방송을 키지 않았어요') + : ListView.builder( + key: UniqueKey(), + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: value.length, + itemBuilder: (context, index) { + final liveDetail = value[index]; - return LiveContainer( - key: Key( - '${liveDetail.channel.channelId}_following'), - autofocus: index == 0 ? true : false, - liveDetail: liveDetail, - ); - }, - ), - ), - ], - ), - AsyncError() => const CenterText(text: '팔로잉 채널을 불러오는데 실패했습니다'), - _ => Column( - children: [ - HeaderText( - text: '팔로잉 라이브 채널', - useShowMoreButton: true, - onPressed: () {}, - ), - const HomeBaseContainer( - child: CenterText(text: '팔로잉 채널 불러오는 중...'), - ), - ], - ), - }; + return LiveContainer( + key: Key('${liveDetail.channel.channelId}_following'), + autofocus: index == 0 ? true : false, + liveDetail: liveDetail, + ); + }, + ), + AsyncError() => const CenterText(text: '팔로잉 채널을 불러오는데 실패했습니다'), + _ => const CenterText(text: '팔로잉 채널 불러오는 중...'), + }, + ), + ); } } diff --git a/lib/src/features/home/widgets/home_function_buttons.dart b/lib/src/features/home/widgets/home_function_buttons.dart new file mode 100644 index 0000000..51531d3 --- /dev/null +++ b/lib/src/features/home/widgets/home_function_buttons.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/constants/styles.dart'; +import '../../../common/widgets/focused_outline_button.dart'; +import '../../../utils/router/app_router.dart'; +import '../../dashboard/controller/dashboard_controller.dart'; +import '../../live/controller/live_controller.dart'; + +class HomeFunctionButtons extends StatelessWidget { + const HomeFunctionButtons({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + // Refresh + _HomeRefreshButton(), + SizedBox(width: 10.0), + // All channels sort by popular + _HomePopularButton(), + ], + ); + } +} + +class _HomeFunctionButton extends StatelessWidget { + const _HomeFunctionButton({ + required this.onPressed, + required this.child, + }); + + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 1, + child: Material( + elevation: 5.0, + color: AppColors.greyContainerColor, + borderRadius: BorderRadius.circular(12.0), + child: FocusedOutlineButton( + padding: const EdgeInsets.all(10.0), + onPressed: onPressed, + child: child, + ), + ), + ); + } +} + +class _HomeRefreshButton extends ConsumerWidget { + const _HomeRefreshButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return _HomeFunctionButton( + onPressed: () { + ref.invalidate(followingLiveControllerProvider); + ref.invalidate(popularLivesControllerProvider); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.refresh, + color: AppColors.whiteColor, + ), + SizedBox(width: 10.0), + Text( + '새로고침', + style: TextStyle( + fontSize: 14.0, + color: AppColors.whiteColor, + ), + ), + ], + ), + ); + } +} + +class _HomePopularButton extends ConsumerWidget { + const _HomePopularButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return _HomeFunctionButton( + onPressed: () { + ref.read(dashboardControllerProvider.notifier).changeScreen( + context, + AppRoute.allLives, + ); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.sort_rounded, + color: AppColors.whiteColor, + ), + SizedBox(width: 10.0), + Text( + '전체 라이브', + style: TextStyle( + fontSize: 14.0, + color: AppColors.whiteColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/features/home/widgets/home_popular_lives.dart b/lib/src/features/home/widgets/home_popular_lives.dart index ea3fd91..fea0650 100644 --- a/lib/src/features/home/widgets/home_popular_lives.dart +++ b/lib/src/features/home/widgets/home_popular_lives.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/widgets/center_text.dart'; -import '../../../common/widgets/header_text.dart'; import '../../live/controller/live_controller.dart'; import '../../live/widgets/live_container.dart'; import './home_base_container.dart'; @@ -13,52 +13,31 @@ class HomePopularLives extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final asyncPopularLives = ref.watch(popularLivesControllerProvider); final scrollController = useScrollController(); - final asyncPopularLives = ref.watch(popularLiveControllerProvider); - - useEffect(() { - scrollController.addListener(() async { - // -50.0: damping - if (scrollController.offset >= - scrollController.position.maxScrollExtent - 50.0 && - !scrollController.position.outOfRange) { - await ref.read(popularLiveControllerProvider.notifier).fetchMore(); - } - }); - return null; - }, [scrollController]); return Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const HeaderText( - text: '인기 채널', - useShowMoreButton: false, - onPressed: null, - ), - HomeBaseContainer( - child: switch (asyncPopularLives) { - AsyncData(:final value) => value == null - ? const CenterText(text: '인기 채널을 불러오는데 실패했습니다') - : ListView.builder( - controller: scrollController, - scrollDirection: Axis.horizontal, - itemCount: value.length, - itemBuilder: (context, index) { - final liveDetail = value[index]; + padding: const EdgeInsets.only(bottom: 10.0), + child: HomeBaseContainer( + child: switch (asyncPopularLives) { + AsyncData(:final value) => value == null + ? const CenterText(text: '인기 채널을 불러오는데 실패했습니다') + : ListView.builder( + key: UniqueKey(), + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: value.length, + itemBuilder: (context, index) { + final liveDetail = value[index]; - return LiveContainer( - liveDetail: liveDetail, - ); - }, - ), - AsyncError() => const CenterText(text: '인기 채널을 불러오는데 실패했습니다'), - _ => const CenterText(text: '인기 채널 불러오는 중...'), - }, - ), - ], + return LiveContainer( + liveDetail: liveDetail, + ); + }, + ), + AsyncError() => const CenterText(text: '인기 채널을 불러오는데 실패했습니다'), + _ => const CenterText(text: '인기 채널 불러오는 중...'), + }, ), ); } diff --git a/lib/src/features/home/widgets/home_refresh_button.dart b/lib/src/features/home/widgets/home_refresh_button.dart deleted file mode 100644 index 6615bf2..0000000 --- a/lib/src/features/home/widgets/home_refresh_button.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../common/constants/styles.dart'; -import '../../../common/widgets/focused_outline_button.dart'; -import '../../live/controller/live_controller.dart'; - -class HomeRefreshButton extends ConsumerWidget { - const HomeRefreshButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Padding( - padding: const EdgeInsets.only(bottom: 10.0), - child: Material( - elevation: 5.0, - color: AppColors.greyContainerColor, - borderRadius: BorderRadius.circular(12.0), - child: FocusedOutlineButton( - padding: const EdgeInsets.all(10.0), - onPressed: () { - ref.invalidate(followingLiveControllerProvider); - ref.invalidate(popularLiveControllerProvider); - }, - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.refresh, - color: AppColors.whiteColor, - ), - SizedBox(width: 10.0), - Text( - '새로고침', - style: TextStyle( - fontSize: 14.0, - color: AppColors.whiteColor, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/features/live/all_lives_screen.dart b/lib/src/features/live/all_lives_screen.dart new file mode 100644 index 0000000..de9d29b --- /dev/null +++ b/lib/src/features/live/all_lives_screen.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../common/constants/dimensions.dart'; +import '../../common/widgets/base_scaffold.dart'; +import '../../common/widgets/header_text.dart'; +import '../../utils/router/app_router.dart'; +import '../dashboard/controller/dashboard_controller.dart'; +import './widgets/all_lives/all_lives_sidebar_buttons.dart'; +import './widgets/all_lives/all_lives_list.dart'; + +class AllLivesScreen extends HookConsumerWidget { + const AllLivesScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + const double horizontalPadding = 16.0; + const double crossAxisSpacing = 5.0; + const int crossAxisCount = 3; + + final double sidebarWidth = MediaQuery.of(context).size.width - + horizontalPadding * 2 - + crossAxisSpacing * 2 * (crossAxisCount - 1) - + Dimensions.liveThumbnailSize.width * crossAxisCount; + + final sidebarFocusNode = useFocusNode(); + + return PopScope( + canPop: false, + onPopInvoked: (_) { + if (sidebarFocusNode.hasFocus && context.mounted) { + ref + .read(dashboardControllerProvider.notifier) + .changeScreen(context, AppRoute.home); + } + sidebarFocusNode.requestFocus(); + }, + child: BaseScaffold( + horizontalPadding: horizontalPadding, + body: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HeaderText( + text: '전체 라이브 둘러보기', + fontSize: 24.0, + ), + Expanded( + child: Row( + children: [ + SizedBox( + width: sidebarWidth, + child: AllLivesSidebarButtons( + sidebarFocusNode: sidebarFocusNode, + ), + ), + const Expanded( + child: AllLivesList( + crossAxisCount: crossAxisCount, + crossAxisSpacing: crossAxisSpacing, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/live/controller/live_controller.dart b/lib/src/features/live/controller/live_controller.dart index bed253f..f6d86fe 100644 --- a/lib/src/features/live/controller/live_controller.dart +++ b/lib/src/features/live/controller/live_controller.dart @@ -3,10 +3,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../auth/controller/auth_controller.dart'; import '../../channel/model/channel.dart'; -import '../../channel/repository/channel_repository.dart'; import '../../following/model/following.dart'; import '../../following/repository/following_repository.dart'; import '../../settings/controller/settings_controller.dart'; +import '../model/all_lives.dart'; import '../model/live.dart'; import '../repository/live_repository.dart'; @@ -52,11 +52,10 @@ class FollowingLiveController extends _$FollowingLiveController { } } -// Home Popular Controller @riverpod -class PopularLiveController extends _$PopularLiveController { +class PopularLivesController extends _$PopularLivesController { Options? _options; - PopularChannelPage? _next; + AllLivesChannelPage? _next; @override FutureOr?> build() async { @@ -68,16 +67,20 @@ class PopularLiveController extends _$PopularLiveController { .read(settingsControllerProvider.notifier) .getPopularChannelsLength(); - return await _initFetch(size: size); + return _initFetch(size: size, sortType: LiveSortType.popular); } - Future?> _initFetch({required int size}) async { - final PopularChannelResponse? channelResponse = - await ref.watch(channelRepositoryProvider).getPopularChannelResponse( + Future?> _initFetch({ + required int size, + required LiveSortType sortType, + }) async { + final AllLivesChannelResponse? channelResponse = + await ref.watch(liveRepositoryProvider).getAllChannelsResponse( options: _options, concurrentUserCount: null, liveId: null, size: size, + sortType: sortType, ); _next = channelResponse?.page; @@ -106,17 +109,115 @@ class PopularLiveController extends _$PopularLiveController { final prev = state.value; state = await AsyncValue.guard(() async { - final response = await ref - .watch(channelRepositoryProvider) - .getPopularChannelResponse( + final response = + await ref.watch(liveRepositoryProvider).getAllChannelsResponse( + options: _options, + concurrentUserCount: _next?.concurrentUserCount, + liveId: _next?.liveId, + ); + + _next = response?.page; + + if (response?.channels == null || _next == null) { + return [...prev!]; + } else { + List liveDetails = []; + + for (Channel channel in response!.channels!) { + final LiveDetail? liveDetail = + await ref.watch(liveRepositoryProvider).getLiveDetail( + channelId: channel.channelId, + options: _options, + ); + + if (liveDetail != null) liveDetails.add(liveDetail); + } + + return [...prev!, ...liveDetails]; + } + }); + } + } +} + +@riverpod +class AllLivesController extends _$AllLivesController { + Options? _options; + AllLivesChannelPage? _next; + + @override + FutureOr?> build() async { + final auth = await ref.watch(authControllerProvider.future); + + _options = auth?.getOptions(); + + final int size = ref + .read(settingsControllerProvider.notifier) + .getPopularChannelsLength(); + + final sortType = ref.watch(currentLiveSortTypeProvider); + + return await _initFetch( + size: size, + sortType: sortType, + ); + } + + Future?> _initFetch({ + required int size, + required LiveSortType sortType, + }) async { + final AllLivesChannelResponse? channelResponse = + await ref.watch(liveRepositoryProvider).getAllChannelsResponse( options: _options, - concurrentUserCount: _next?.concurrentUserCount, - liveId: _next?.liveId, + concurrentUserCount: null, + liveId: null, + size: size, + sortType: sortType, ); + _next = channelResponse?.page; + + if (channelResponse?.channels != null) { + List liveDetails = []; + + for (Channel channel in channelResponse!.channels!) { + final LiveDetail? liveDetail = + await ref.watch(liveRepositoryProvider).getLiveDetail( + channelId: channel.channelId, + options: _options, + ); + + if (liveDetail != null) liveDetails.add(liveDetail); + } + + return liveDetails; + } + + return null; + } + + Future fetchMore() async { + if (_next != null) { + final prev = state.value; + + // Show loading state in all lives page + ref.read(allLivesLoadingStateProvider.notifier).setState(true); + + state = await AsyncValue.guard(() async { + final response = + await ref.watch(liveRepositoryProvider).getAllChannelsResponse( + options: _options, + concurrentUserCount: _next?.concurrentUserCount, + liveId: _next?.liveId, + ); + _next = response?.page; if (response?.channels == null || _next == null) { + // Show loading state in all lives page + ref.read(allLivesLoadingStateProvider.notifier).setState(false); + return [...prev!]; } else { List liveDetails = []; @@ -131,9 +232,36 @@ class PopularLiveController extends _$PopularLiveController { if (liveDetail != null) liveDetails.add(liveDetail); } + // Show loading state in all lives page + ref.read(allLivesLoadingStateProvider.notifier).setState(false); + return [...prev!, ...liveDetails]; } }); } } } + +@riverpod +class AllLivesLoadingState extends _$AllLivesLoadingState { + @override + bool build() { + return false; + } + + void setState(bool value) { + state = value; + } +} + +@riverpod +class CurrentLiveSortType extends _$CurrentLiveSortType { + @override + LiveSortType build() { + return LiveSortType.popular; + } + + void setState(LiveSortType sortType) { + if (state != sortType) state = sortType; + } +} diff --git a/lib/src/features/live/controller/live_controller.g.dart b/lib/src/features/live/controller/live_controller.g.dart index a3a239c..0750194 100644 --- a/lib/src/features/live/controller/live_controller.g.dart +++ b/lib/src/features/live/controller/live_controller.g.dart @@ -23,22 +23,73 @@ final followingLiveControllerProvider = AutoDisposeAsyncNotifierProvider< ); typedef _$FollowingLiveController = AutoDisposeAsyncNotifier?>; -String _$popularLiveControllerHash() => - r'ae733ea28166cbc9733608e91fcfdf5b2bed9e52'; - -/// See also [PopularLiveController]. -@ProviderFor(PopularLiveController) -final popularLiveControllerProvider = AutoDisposeAsyncNotifierProvider< - PopularLiveController, List?>.internal( - PopularLiveController.new, - name: r'popularLiveControllerProvider', +String _$popularLivesControllerHash() => + r'768408111fa907644b7006b3587a0913f2b89bb5'; + +/// See also [PopularLivesController]. +@ProviderFor(PopularLivesController) +final popularLivesControllerProvider = AutoDisposeAsyncNotifierProvider< + PopularLivesController, List?>.internal( + PopularLivesController.new, + name: r'popularLivesControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$popularLivesControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PopularLivesController = AutoDisposeAsyncNotifier?>; +String _$allLivesControllerHash() => + r'9ab8587332e53abce440fa094444bc0bd18fe6dc'; + +/// See also [AllLivesController]. +@ProviderFor(AllLivesController) +final allLivesControllerProvider = AutoDisposeAsyncNotifierProvider< + AllLivesController, List?>.internal( + AllLivesController.new, + name: r'allLivesControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$allLivesControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AllLivesController = AutoDisposeAsyncNotifier?>; +String _$allLivesLoadingStateHash() => + r'a4ee4e0058549b3a4ee1446c969c25d66d451d5f'; + +/// See also [AllLivesLoadingState]. +@ProviderFor(AllLivesLoadingState) +final allLivesLoadingStateProvider = + AutoDisposeNotifierProvider.internal( + AllLivesLoadingState.new, + name: r'allLivesLoadingStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$allLivesLoadingStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AllLivesLoadingState = AutoDisposeNotifier; +String _$currentLiveSortTypeHash() => + r'2ca9bf40d5e7e49e3599d12f26f51fdc9af7a2ec'; + +/// See also [CurrentLiveSortType]. +@ProviderFor(CurrentLiveSortType) +final currentLiveSortTypeProvider = + AutoDisposeNotifierProvider.internal( + CurrentLiveSortType.new, + name: r'currentLiveSortTypeProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$popularLiveControllerHash, + : _$currentLiveSortTypeHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$PopularLiveController = AutoDisposeAsyncNotifier?>; +typedef _$CurrentLiveSortType = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/live/model/all_lives.dart b/lib/src/features/live/model/all_lives.dart new file mode 100644 index 0000000..9da1ae0 --- /dev/null +++ b/lib/src/features/live/model/all_lives.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../channel/model/channel.dart'; + +part 'all_lives.freezed.dart'; +part 'all_lives.g.dart'; + +@freezed +class AllLivesChannelPage with _$AllLivesChannelPage { + factory AllLivesChannelPage({ + required int? concurrentUserCount, + required int? liveId, + }) = _AllLivesChannelPage; + + factory AllLivesChannelPage.fromJson(Map json) => + _$AllLivesChannelPageFromJson(json); +} + +@freezed +class AllLivesChannelResponse with _$AllLivesChannelResponse { + const factory AllLivesChannelResponse({ + required List? channels, + required AllLivesChannelPage? page, + }) = _AllLivesChannelResponse; +} diff --git a/lib/src/features/live/model/all_lives.freezed.dart b/lib/src/features/live/model/all_lives.freezed.dart new file mode 100644 index 0000000..0bae132 --- /dev/null +++ b/lib/src/features/live/model/all_lives.freezed.dart @@ -0,0 +1,341 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'all_lives.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +AllLivesChannelPage _$AllLivesChannelPageFromJson(Map json) { + return _AllLivesChannelPage.fromJson(json); +} + +/// @nodoc +mixin _$AllLivesChannelPage { + int? get concurrentUserCount => throw _privateConstructorUsedError; + int? get liveId => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $AllLivesChannelPageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AllLivesChannelPageCopyWith<$Res> { + factory $AllLivesChannelPageCopyWith( + AllLivesChannelPage value, $Res Function(AllLivesChannelPage) then) = + _$AllLivesChannelPageCopyWithImpl<$Res, AllLivesChannelPage>; + @useResult + $Res call({int? concurrentUserCount, int? liveId}); +} + +/// @nodoc +class _$AllLivesChannelPageCopyWithImpl<$Res, $Val extends AllLivesChannelPage> + implements $AllLivesChannelPageCopyWith<$Res> { + _$AllLivesChannelPageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? concurrentUserCount = freezed, + Object? liveId = freezed, + }) { + return _then(_value.copyWith( + concurrentUserCount: freezed == concurrentUserCount + ? _value.concurrentUserCount + : concurrentUserCount // ignore: cast_nullable_to_non_nullable + as int?, + liveId: freezed == liveId + ? _value.liveId + : liveId // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AllLivesChannelPageImplCopyWith<$Res> + implements $AllLivesChannelPageCopyWith<$Res> { + factory _$$AllLivesChannelPageImplCopyWith(_$AllLivesChannelPageImpl value, + $Res Function(_$AllLivesChannelPageImpl) then) = + __$$AllLivesChannelPageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int? concurrentUserCount, int? liveId}); +} + +/// @nodoc +class __$$AllLivesChannelPageImplCopyWithImpl<$Res> + extends _$AllLivesChannelPageCopyWithImpl<$Res, _$AllLivesChannelPageImpl> + implements _$$AllLivesChannelPageImplCopyWith<$Res> { + __$$AllLivesChannelPageImplCopyWithImpl(_$AllLivesChannelPageImpl _value, + $Res Function(_$AllLivesChannelPageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? concurrentUserCount = freezed, + Object? liveId = freezed, + }) { + return _then(_$AllLivesChannelPageImpl( + concurrentUserCount: freezed == concurrentUserCount + ? _value.concurrentUserCount + : concurrentUserCount // ignore: cast_nullable_to_non_nullable + as int?, + liveId: freezed == liveId + ? _value.liveId + : liveId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AllLivesChannelPageImpl implements _AllLivesChannelPage { + _$AllLivesChannelPageImpl( + {required this.concurrentUserCount, required this.liveId}); + + factory _$AllLivesChannelPageImpl.fromJson(Map json) => + _$$AllLivesChannelPageImplFromJson(json); + + @override + final int? concurrentUserCount; + @override + final int? liveId; + + @override + String toString() { + return 'AllLivesChannelPage(concurrentUserCount: $concurrentUserCount, liveId: $liveId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AllLivesChannelPageImpl && + (identical(other.concurrentUserCount, concurrentUserCount) || + other.concurrentUserCount == concurrentUserCount) && + (identical(other.liveId, liveId) || other.liveId == liveId)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, concurrentUserCount, liveId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AllLivesChannelPageImplCopyWith<_$AllLivesChannelPageImpl> get copyWith => + __$$AllLivesChannelPageImplCopyWithImpl<_$AllLivesChannelPageImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$AllLivesChannelPageImplToJson( + this, + ); + } +} + +abstract class _AllLivesChannelPage implements AllLivesChannelPage { + factory _AllLivesChannelPage( + {required final int? concurrentUserCount, + required final int? liveId}) = _$AllLivesChannelPageImpl; + + factory _AllLivesChannelPage.fromJson(Map json) = + _$AllLivesChannelPageImpl.fromJson; + + @override + int? get concurrentUserCount; + @override + int? get liveId; + @override + @JsonKey(ignore: true) + _$$AllLivesChannelPageImplCopyWith<_$AllLivesChannelPageImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AllLivesChannelResponse { + List? get channels => throw _privateConstructorUsedError; + AllLivesChannelPage? get page => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $AllLivesChannelResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AllLivesChannelResponseCopyWith<$Res> { + factory $AllLivesChannelResponseCopyWith(AllLivesChannelResponse value, + $Res Function(AllLivesChannelResponse) then) = + _$AllLivesChannelResponseCopyWithImpl<$Res, AllLivesChannelResponse>; + @useResult + $Res call({List? channels, AllLivesChannelPage? page}); + + $AllLivesChannelPageCopyWith<$Res>? get page; +} + +/// @nodoc +class _$AllLivesChannelResponseCopyWithImpl<$Res, + $Val extends AllLivesChannelResponse> + implements $AllLivesChannelResponseCopyWith<$Res> { + _$AllLivesChannelResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channels = freezed, + Object? page = freezed, + }) { + return _then(_value.copyWith( + channels: freezed == channels + ? _value.channels + : channels // ignore: cast_nullable_to_non_nullable + as List?, + page: freezed == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as AllLivesChannelPage?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AllLivesChannelPageCopyWith<$Res>? get page { + if (_value.page == null) { + return null; + } + + return $AllLivesChannelPageCopyWith<$Res>(_value.page!, (value) { + return _then(_value.copyWith(page: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AllLivesChannelResponseImplCopyWith<$Res> + implements $AllLivesChannelResponseCopyWith<$Res> { + factory _$$AllLivesChannelResponseImplCopyWith( + _$AllLivesChannelResponseImpl value, + $Res Function(_$AllLivesChannelResponseImpl) then) = + __$$AllLivesChannelResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List? channels, AllLivesChannelPage? page}); + + @override + $AllLivesChannelPageCopyWith<$Res>? get page; +} + +/// @nodoc +class __$$AllLivesChannelResponseImplCopyWithImpl<$Res> + extends _$AllLivesChannelResponseCopyWithImpl<$Res, + _$AllLivesChannelResponseImpl> + implements _$$AllLivesChannelResponseImplCopyWith<$Res> { + __$$AllLivesChannelResponseImplCopyWithImpl( + _$AllLivesChannelResponseImpl _value, + $Res Function(_$AllLivesChannelResponseImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channels = freezed, + Object? page = freezed, + }) { + return _then(_$AllLivesChannelResponseImpl( + channels: freezed == channels + ? _value._channels + : channels // ignore: cast_nullable_to_non_nullable + as List?, + page: freezed == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as AllLivesChannelPage?, + )); + } +} + +/// @nodoc + +class _$AllLivesChannelResponseImpl implements _AllLivesChannelResponse { + const _$AllLivesChannelResponseImpl( + {required final List? channels, required this.page}) + : _channels = channels; + + final List? _channels; + @override + List? get channels { + final value = _channels; + if (value == null) return null; + if (_channels is EqualUnmodifiableListView) return _channels; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + final AllLivesChannelPage? page; + + @override + String toString() { + return 'AllLivesChannelResponse(channels: $channels, page: $page)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AllLivesChannelResponseImpl && + const DeepCollectionEquality().equals(other._channels, _channels) && + (identical(other.page, page) || other.page == page)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_channels), page); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AllLivesChannelResponseImplCopyWith<_$AllLivesChannelResponseImpl> + get copyWith => __$$AllLivesChannelResponseImplCopyWithImpl< + _$AllLivesChannelResponseImpl>(this, _$identity); +} + +abstract class _AllLivesChannelResponse implements AllLivesChannelResponse { + const factory _AllLivesChannelResponse( + {required final List? channels, + required final AllLivesChannelPage? page}) = + _$AllLivesChannelResponseImpl; + + @override + List? get channels; + @override + AllLivesChannelPage? get page; + @override + @JsonKey(ignore: true) + _$$AllLivesChannelResponseImplCopyWith<_$AllLivesChannelResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/src/features/live/model/all_lives.g.dart b/lib/src/features/live/model/all_lives.g.dart new file mode 100644 index 0000000..30a1663 --- /dev/null +++ b/lib/src/features/live/model/all_lives.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'all_lives.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AllLivesChannelPageImpl _$$AllLivesChannelPageImplFromJson( + Map json) => + _$AllLivesChannelPageImpl( + concurrentUserCount: json['concurrentUserCount'] as int?, + liveId: json['liveId'] as int?, + ); + +Map _$$AllLivesChannelPageImplToJson( + _$AllLivesChannelPageImpl instance) => + { + 'concurrentUserCount': instance.concurrentUserCount, + 'liveId': instance.liveId, + }; diff --git a/lib/src/features/live/repository/live_repository.dart b/lib/src/features/live/repository/live_repository.dart index 64b5e4d..9258043 100644 --- a/lib/src/features/live/repository/live_repository.dart +++ b/lib/src/features/live/repository/live_repository.dart @@ -3,9 +3,20 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:unofficial_chzzk_android_tv/src/features/live/model/live.dart'; import '../../../common/constants/api.dart'; +import '../../channel/model/channel.dart'; +import '../model/all_lives.dart'; part 'live_repository.g.dart'; +enum LiveSortType { + latest('LATEST'), + popular('POPULAR'); + + final String sortType; + + const LiveSortType(this.sortType); +} + @riverpod LiveRepository liveRepository(LiveRepositoryRef ref) => LiveRepository(); @@ -34,4 +45,43 @@ class LiveRepository { return jsonData == null ? null : LiveDetail.fromJson(jsonData); } + + Future getAllChannelsResponse({ + required Options? options, + required int? concurrentUserCount, + required int? liveId, + int size = 18, + LiveSortType sortType = LiveSortType.popular, + }) async { + final url = APIUrl.allLives( + concurrentUserCount: concurrentUserCount, + liveId: liveId, + size: size, + sortType: sortType, + ); + + final response = await _dio.get(url, options: options); + + final Map? pageResponse = response.data['content']['page']; + final List channelsResponse = response.data['content']['data']; + + final AllLivesChannelPage? page = pageResponse == null + ? null + : AllLivesChannelPage.fromJson(pageResponse['next']); + final List channels = channelsResponse + // remove blocked channel + .where((response) { + final personalData = response['channel']['personalData']; + if (personalData == null) return true; + + return personalData['privateUserBlock'] != true; + }) + .map((response) => Channel.fromJson(response['channel'])) + .toList(); + + return AllLivesChannelResponse( + channels: channels, + page: page, + ); + } } diff --git a/lib/src/features/live/widgets/all_lives/all_lives_list.dart b/lib/src/features/live/widgets/all_lives/all_lives_list.dart new file mode 100644 index 0000000..8e89c42 --- /dev/null +++ b/lib/src/features/live/widgets/all_lives/all_lives_list.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../../common/constants/dimensions.dart'; +import '../../../../common/widgets/center_text.dart'; +import '../../controller/live_controller.dart'; +import '../live_container.dart'; + +class AllLivesList extends HookConsumerWidget { + const AllLivesList({ + super.key, + required this.crossAxisCount, + required this.crossAxisSpacing, + }); + + final int crossAxisCount; + final double crossAxisSpacing; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final scrollController = useScrollController(); + final asyncLives = ref.watch(allLivesControllerProvider); + + useEffect(() { + scrollController.addListener(() async { + // -15.0: damping + if (scrollController.offset >= + scrollController.position.maxScrollExtent - 15.0 && + !scrollController.position.outOfRange) { + await ref.read(allLivesControllerProvider.notifier).fetchMore(); + } + }); + return null; + }, [scrollController]); + + return switch (asyncLives) { + AsyncData(:final value) => (value == null) + ? const CenterText(text: '라이브를 불러올 수 없습니다') + : GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: crossAxisSpacing, + mainAxisSpacing: 15.0, + mainAxisExtent: Dimensions.homeBaseContainerHeight, + ), + controller: scrollController, + itemCount: value.length, + itemBuilder: (context, index) { + final liveDetail = value[index]; + + return LiveContainer( + autofocus: index == 0 ? true : false, + liveDetail: liveDetail, + ); + }, + ), + AsyncError() => const CenterText(text: '라이브를 불러올 수 없습니다'), + _ => const CenterText(text: '라이브 불러오는 중...'), + }; + } +} diff --git a/lib/src/features/live/widgets/all_lives/all_lives_sidebar_buttons.dart b/lib/src/features/live/widgets/all_lives/all_lives_sidebar_buttons.dart new file mode 100644 index 0000000..3e7fa06 --- /dev/null +++ b/lib/src/features/live/widgets/all_lives/all_lives_sidebar_buttons.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../../common/constants/styles.dart'; +import '../../../../common/widgets/focused_outline_button.dart'; +import '../../controller/live_controller.dart'; +import '../../repository/live_repository.dart'; + +class AllLivesSidebarButtons extends HookConsumerWidget { + const AllLivesSidebarButtons({ + super.key, + required this.sidebarFocusNode, + }); + + final FocusNode sidebarFocusNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final liveSortType = ref.watch(currentLiveSortTypeProvider); + final int currentIndex = liveSortType == LiveSortType.popular ? 0 : 1; + + const int itemCount = 2; + + final focusNodes = List.generate( + itemCount, + (_) => useFocusNode(), + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: Focus( + focusNode: sidebarFocusNode, + onFocusChange: (value) { + if (value) focusNodes[currentIndex].requestFocus(); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: ListView.builder( + itemCount: itemCount, + itemBuilder: (context, index) { + return FocusedOutlineButton( + focusNode: focusNodes[index], + padding: const EdgeInsets.all(10.0), + onPressed: () { + if (currentIndex != index) { + ref.read(currentLiveSortTypeProvider.notifier).setState( + index == 0 + ? LiveSortType.popular + : LiveSortType.latest, + ); + } + }, + child: Center( + child: Text( + index == 0 ? '인기순' : '최신순', + style: TextStyle( + color: currentIndex == index + ? AppColors.chzzkColor + : AppColors.whiteColor, + ), + ), + ), + ); + }, + ), + ), + Consumer( + builder: (BuildContext context, WidgetRef ref, Widget? child) { + final loadingState = ref.watch(allLivesLoadingStateProvider); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Text(loadingState == true ? '로딩중...' : ' '), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/live_streaming/widgets/chat_container.dart b/lib/src/features/live_streaming/widgets/chat_container.dart index de5660b..fdc0c76 100644 --- a/lib/src/features/live_streaming/widgets/chat_container.dart +++ b/lib/src/features/live_streaming/widgets/chat_container.dart @@ -23,12 +23,10 @@ class ChatContainer extends StatelessWidget { if (chat.emojis == null) { textSpans.add( - WidgetSpan( - child: Text( - chat.msg, - style: TextStyle( - fontSize: fontSize, - ), + TextSpan( + text: chat.msg, + style: TextStyle( + fontSize: fontSize, ), ), ); diff --git a/lib/src/features/settings/controller/settings_controller.dart b/lib/src/features/settings/controller/settings_controller.dart index 275d6d4..1148cf6 100644 --- a/lib/src/features/settings/controller/settings_controller.dart +++ b/lib/src/features/settings/controller/settings_controller.dart @@ -35,4 +35,3 @@ class SettingsController extends _$SettingsController { return _settingRepository.getPopularChannelsLength(); } } - diff --git a/lib/src/features/settings/controller/update_controller.dart b/lib/src/features/settings/controller/update_controller.dart new file mode 100644 index 0000000..28cfdf4 --- /dev/null +++ b/lib/src/features/settings/controller/update_controller.dart @@ -0,0 +1,27 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../repository/update_repository.dart'; + +part 'update_controller.g.dart'; + +@riverpod +class UpdateController extends _$UpdateController { + @override + void build() { + return; + } + + Future checkUpdate() async { + final latest = await ref.watch(updateRepositoryProvider).getLatestVersion(); + + if (latest != 'error') { + return latest['tag_name']; + } else { + return 'error'; + } + } + + Future> findSupportedABI() async { + return await ref.watch(updateRepositoryProvider).getDeviceAbi(); + } +} diff --git a/lib/src/features/settings/controller/update_controller.g.dart b/lib/src/features/settings/controller/update_controller.g.dart new file mode 100644 index 0000000..5468b9e --- /dev/null +++ b/lib/src/features/settings/controller/update_controller.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'update_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$updateControllerHash() => r'b6fa5e26990b2c26839c13e62b66812ca2cfa067'; + +/// See also [UpdateController]. +@ProviderFor(UpdateController) +final updateControllerProvider = + AutoDisposeNotifierProvider.internal( + UpdateController.new, + name: r'updateControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$updateControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$UpdateController = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/settings/repository/settings_repository.dart b/lib/src/features/settings/repository/settings_repository.dart index ec99d74..af3dac8 100644 --- a/lib/src/features/settings/repository/settings_repository.dart +++ b/lib/src/features/settings/repository/settings_repository.dart @@ -127,6 +127,6 @@ class SettingsRepository { int getPopularChannelsLength() { return _sharedPreferences .getInt(SharedPrefencesKey.popularChannelsLength) ?? - 20; + 18; } } diff --git a/lib/src/features/settings/repository/update_repository.dart b/lib/src/features/settings/repository/update_repository.dart new file mode 100644 index 0000000..44b31dd --- /dev/null +++ b/lib/src/features/settings/repository/update_repository.dart @@ -0,0 +1,60 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../../common/constants/api.dart'; + +part 'update_repository.g.dart'; + +enum ABI { + v7a('v7a'), + v8a('v8a'), + any('any'); + + final String abi; + + const ABI(this.abi); +} + +@riverpod +UpdateRepository updateRepository(UpdateRepositoryRef ref) => + UpdateRepository(); + +class UpdateRepository { + final Dio _dio = Dio(); + + Future getLatestVersion() async { + final response = await _dio.get( + APIUrl.latestApp(), + ); + + if (response.statusCode == 200) { + return response.data; + } else { + return 'error'; + } + } + + Future> getDeviceAbi() async { + final deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final List supportedAbis = androidInfo.supportedAbis; + + final bool containsV7A = supportedAbis.any((abi) => abi.contains('v7a')); + final bool containsV8A = supportedAbis.any((abi) => abi.contains('v8a')); + + if (containsV7A && containsV8A) { + return [ABI.v7a, ABI.v8a]; + } else if (containsV7A) { + return [ABI.v7a]; + } else if (containsV8A) { + return [ABI.v8a]; + } else { + return [ABI.any]; + } + } + + // Future updateApp() async {} + // Future _downloadApk() async {} + // Future _installApk() async {} +} diff --git a/lib/src/features/settings/repository/update_repository.g.dart b/lib/src/features/settings/repository/update_repository.g.dart new file mode 100644 index 0000000..6369688 --- /dev/null +++ b/lib/src/features/settings/repository/update_repository.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'update_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$updateRepositoryHash() => r'e7a1b5f6b275fd2add05b1caf6de997bf93703aa'; + +/// See also [updateRepository]. +@ProviderFor(updateRepository) +final updateRepositoryProvider = AutoDisposeProvider.internal( + updateRepository, + name: r'updateRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$updateRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UpdateRepositoryRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart index 8659343..9b57434 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/settings_screen.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../common/constants/app_version.dart'; import '../../common/widgets/focused_outline_button.dart'; import '../../common/widgets/header_text.dart'; +import '../../utils/popup/popup_utils.dart'; import '../../utils/router/app_router.dart'; import '../dashboard/dashboard_screen.dart'; +import './controller/update_controller.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @@ -22,35 +25,99 @@ class SettingsScreen extends StatelessWidget { text: '오픈소스 라이선스', fontSize: 18.0, ), - FocusedOutlineButton( + _functionButton( autofocus: true, - padding: const EdgeInsets.all(10.0), - onPressed: () { + '보기', + () { context.pushNamed(AppRoute.license.routeName); }, - child: const Text('보기'), ), const Divider(), const HeaderText( text: 'WebView 로그인', fontSize: 18.0, ), - FocusedOutlineButton( - padding: const EdgeInsets.all(10.0), - onPressed: () { + _functionButton( + 'WebView 로그인', + () { context.pushNamed(AppRoute.naverLoginWithWebView.routeName); }, - child: const Text('WebView 로그인'), ), const Divider(), const HeaderText( - text: '앱 버전', + text: '앱 버전: ${AppVersion.version}', fontSize: 18.0, ), - const Text(AppVersion.version), + Consumer( + builder: (context, ref, child) { + return _functionButton( + '업데이트 확인', + () async { + final latestVersion = await ref + .read(updateControllerProvider.notifier) + .checkUpdate(); + + final supportedAbis = await ref + .read(updateControllerProvider.notifier) + .findSupportedABI(); + + String msg = + '새로운 업데이트가 있습니다. GitHub Repository를 방문해주세요.\nhttps://github.com/Escaper-Park/unofficial_chzzk_android_tv\n지원 ABI: $supportedAbis'; + if (latestVersion == 'error' && context.mounted) { + await PopupUtils.showSingleDialog( + context: context, + titleText: '업데이트 확인 오류', + contentText: '오류 발생', + ); + + return; + } + + if (latestVersion == AppVersion.version && + context.mounted) { + await PopupUtils.showSingleDialog( + context: context, + titleText: '최신 버전', + contentText: '최신 버전을 사용 중입니다', + ); + + return; + } + + if (latestVersion != AppVersion.version && + context.mounted) { + await PopupUtils.showSingleDialog( + context: context, + titleText: '업데이트 발견: $latestVersion', + contentHeight: 200.0, + contentText: msg, + ); + + return; + } + }, + ); + }, + ), ], ), ), ); } + + Widget _functionButton( + String text, + VoidCallback onPressed, { + bool autofocus = false, + }) { + return Padding( + padding: const EdgeInsets.all(5.0), + child: FocusedOutlineButton( + padding: const EdgeInsets.all(10.0), + autofocus: autofocus, + onPressed: onPressed, + child: Text(text), + ), + ); + } } diff --git a/lib/src/utils/focus/dpad_widget.dart b/lib/src/utils/focus/dpad_widget.dart index ee083d7..987f3fa 100644 --- a/lib/src/utils/focus/dpad_widget.dart +++ b/lib/src/utils/focus/dpad_widget.dart @@ -23,6 +23,7 @@ class DpadWidget extends HookWidget { this.borderRadius = 10.0, this.useKeyUpEvent = false, this.padding = EdgeInsets.zero, + this.margin = EdgeInsets.zero, this.focusNode, }); @@ -33,6 +34,7 @@ class DpadWidget extends HookWidget { final double borderRadius; final bool useKeyUpEvent; final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry margin; final FocusNode? focusNode; @override @@ -60,6 +62,7 @@ class DpadWidget extends HookWidget { }, child: useFocusedBorder ? Container( + margin: margin, padding: padding, decoration: BoxDecoration( border: Border.all( diff --git a/lib/src/utils/router/app_router.dart b/lib/src/utils/router/app_router.dart index 3565201..6a1f07b 100644 --- a/lib/src/utils/router/app_router.dart +++ b/lib/src/utils/router/app_router.dart @@ -46,6 +46,9 @@ enum AppRoute { // Multi_view Streaming multiViewStreaming('multiViewStreaming', 'multiViewStreaming', 13), + // allLives channels + allLives('allLives', 'allLives', 14), + // Login (Webview) naverLoginWithWebView('naverLoginWithWebView', 'naverLoginWithWebView', 97), @@ -228,6 +231,18 @@ Raw appRouter(AppRouterRef ref) { ); }, ), + GoRoute( + path: AppRoute.allLives.routePath, + name: AppRoute.allLives.routeName, + pageBuilder: (context, state) { + return NoTransitionPage( + child: AllLivesScreen( + key: state.pageKey, + ), + ); + }, + ), + // NaverLogin With WebView GoRoute( path: AppRoute.naverLoginWithWebView.routePath, diff --git a/lib/src/utils/router/app_router.g.dart b/lib/src/utils/router/app_router.g.dart index 6e53232..addd525 100644 --- a/lib/src/utils/router/app_router.g.dart +++ b/lib/src/utils/router/app_router.g.dart @@ -6,7 +6,7 @@ part of 'app_router.dart'; // RiverpodGenerator // ************************************************************************** -String _$appRouterHash() => r'edab6bedfca6bdf7b0c122010810de4f14e6c4cf'; +String _$appRouterHash() => r'891f643700d79aee352fbbd56b7c1ce5b304002f'; /// See also [appRouter]. @ProviderFor(appRouter) diff --git a/lib/src/utils/router/screens.dart b/lib/src/utils/router/screens.dart index 7fde5ce..fdc0bc8 100644 --- a/lib/src/utils/router/screens.dart +++ b/lib/src/utils/router/screens.dart @@ -9,16 +9,10 @@ export '../../features/settings/settings_screen.dart'; export '../../features/auth/id_input_screen.dart'; export '../../features/auth/password_input_screen.dart'; export '../../features/vod/vod_screen.dart'; - -// Multiview selection export '../../features/multi_view/multi_view_screen.dart'; - export '../../features/live_streaming/live_streaming_screen.dart'; export '../../features/vod_streaming/vod_streaming_screen.dart'; - export '../../features/multi_view_streaming/multi_view_streaming_screen.dart'; - export '../../features/settings/widgets/open_source_license_screen.dart'; - -// Login Webview export '../../features/auth/naver_login_with_webview_screen.dart'; +export '../../features/live/all_lives_screen.dart'; diff --git a/lib/src/utils/video_player/controller/live_stream_controller.dart b/lib/src/utils/video_player/controller/live_stream_controller.dart index d19bae7..cd50ae6 100644 --- a/lib/src/utils/video_player/controller/live_stream_controller.dart +++ b/lib/src/utils/video_player/controller/live_stream_controller.dart @@ -43,7 +43,6 @@ class LiveStreamController extends _$LiveStreamController { // Start pause Timer ref.read(pauseTimerProvider.notifier).pauseAndStartTimer(); } - // Play else { // Play back from pause @@ -66,6 +65,7 @@ class LiveStreamController extends _$LiveStreamController { ) async { controller.pause(); ref.read(controlOverlayTimerProvider.notifier).cancelTimer(); + ref.read(overlayControllerProvider.notifier).setState(OverlayType.none); if (context.mounted) { context.pushReplacementNamed( diff --git a/lib/src/utils/video_player/controller/live_stream_controller.g.dart b/lib/src/utils/video_player/controller/live_stream_controller.g.dart index 7afa359..304efa2 100644 --- a/lib/src/utils/video_player/controller/live_stream_controller.g.dart +++ b/lib/src/utils/video_player/controller/live_stream_controller.g.dart @@ -7,7 +7,7 @@ part of 'live_stream_controller.dart'; // ************************************************************************** String _$liveStreamControllerHash() => - r'170dc49da59f02a1700087a81bc13fee1c1e3d16'; + r'5389b0a1c8ae6936ee575306c75c3bee9022c238'; /// See also [LiveStreamController]. @ProviderFor(LiveStreamController) diff --git a/lib/src/utils/video_player/widgets/common/live_explore_error_button.dart b/lib/src/utils/video_player/widgets/common/live_explore_error_button.dart new file mode 100644 index 0000000..87f014f --- /dev/null +++ b/lib/src/utils/video_player/widgets/common/live_explore_error_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/widgets/center_text.dart'; +import '../../../../common/widgets/focused_outline_button.dart'; + +class LiveExploreErrorButton extends StatelessWidget { + const LiveExploreErrorButton({ + super.key, + required this.text, + required this.onPressed, + }); + + final String text; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: IntrinsicWidth( + child: IntrinsicHeight( + child: FocusedOutlineButton( + padding: const EdgeInsets.all(10.0), + autofocus: true, + onPressed: onPressed, + child: CenterText(text: text), + ), + ), + ), + ); + } +} diff --git a/lib/src/utils/video_player/widgets/live/category/live_stream_category_live_list.dart b/lib/src/utils/video_player/widgets/live/category/live_stream_category_live_list.dart index 4cced05..e54ae7c 100644 --- a/lib/src/utils/video_player/widgets/live/category/live_stream_category_live_list.dart +++ b/lib/src/utils/video_player/widgets/live/category/live_stream_category_live_list.dart @@ -1,12 +1,11 @@ -import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:chewie/chewie.dart'; import '../../../../../common/constants/dimensions.dart'; import '../../../../../common/constants/styles.dart'; import '../../../../../common/widgets/center_text.dart'; -import '../../../../../common/widgets/focused_outline_button.dart'; import '../../../../../common/widgets/header_text.dart'; import '../../../../../features/category/controller/category_live_controller.dart'; import '../../../../../features/category/model/category.dart'; @@ -17,6 +16,7 @@ import '../../../../focus/dpad_widget.dart'; import '../../../../popup/popup_utils.dart'; import '../../../controller/live_stream_controller.dart'; import '../../../controller/network_video_controller.dart'; +import '../../common/live_explore_error_button.dart'; class LiveStreamCategoryLiveList extends HookConsumerWidget { const LiveStreamCategoryLiveList({ @@ -129,32 +129,44 @@ class LiveStreamCategoryLiveList extends HookConsumerWidget { const HeaderText(text: '카테고리 라이브 채널'), HomeBaseContainer( child: switch (asyncCategoryLives) { - AsyncData(:final value) => - (value == null || value.isEmpty) - ? const CenterText(text: '카테고리 채널에 동영상이 없습니다') - : ListView.builder( - controller: scrollController, - scrollDirection: Axis.horizontal, - itemCount: value.length, - itemBuilder: (context, index) { - final liveDetail = value[index]; + AsyncData(:final value) => (value == null || + value.isEmpty) + ? LiveExploreErrorButton( + text: '해당 카테고리에 영상이 없습니다', + onPressed: () { + ref + .read( + controlOverlayTimerProvider.notifier) + .showOverlayAndStartTimer( + videoFocusNode: videoFocusNode, + seconds: 0, + overlayType: OverlayType.category, + ); + }, + ) + : ListView.builder( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: value.length, + itemBuilder: (context, index) { + final liveDetail = value[index]; - return LiveContainer( - autofocus: index == 0 ? true : false, - liveDetail: liveDetail, - onPressed: () async { - await watchLive( - context, - ref, - liveDetail, - category, - ); - }, - ); - }, - ), - AsyncError() => FocusedOutlineButton( - autofocus: true, + return LiveContainer( + autofocus: index == 0 ? true : false, + liveDetail: liveDetail, + onPressed: () async { + await watchLive( + context, + ref, + liveDetail, + category, + ); + }, + ); + }, + ), + AsyncError() => LiveExploreErrorButton( + text: '카테고리 채널을 불러오는데 실패했거나, 카테고리가 설정되어 있지 않습니다.', onPressed: () { ref .read(controlOverlayTimerProvider.notifier) @@ -164,9 +176,6 @@ class LiveStreamCategoryLiveList extends HookConsumerWidget { overlayType: OverlayType.category, ); }, - child: const CenterText( - text: - '카테고리 채널을 불러오는데 실패했거나, 카테고리가 설정되어 있지 않습니다.'), ), _ => const CenterText(text: '카테고리 채널 불러오는 중...'), }, diff --git a/lib/src/utils/video_player/widgets/live/following/live_stream_following_list.dart b/lib/src/utils/video_player/widgets/live/following/live_stream_following_list.dart index 1d6c0dd..dfe140f 100644 --- a/lib/src/utils/video_player/widgets/live/following/live_stream_following_list.dart +++ b/lib/src/utils/video_player/widgets/live/following/live_stream_following_list.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../../../common/constants/dimensions.dart'; import '../../../../../common/constants/styles.dart'; import '../../../../../common/widgets/center_text.dart'; -import '../../../../../common/widgets/focused_outline_button.dart'; import '../../../../../common/widgets/header_text.dart'; import '../../../../../features/home/widgets/home_base_container.dart'; import '../../../../../features/live/controller/live_controller.dart'; @@ -15,6 +14,7 @@ import '../../../../focus/dpad_widget.dart'; import '../../../../popup/popup_utils.dart'; import '../../../controller/live_stream_controller.dart'; import '../../../controller/network_video_controller.dart'; +import '../../common/live_explore_error_button.dart'; class LiveStreamFollowingList extends HookConsumerWidget { const LiveStreamFollowingList({ @@ -101,8 +101,8 @@ class LiveStreamFollowingList extends HookConsumerWidget { child: switch (asyncFollowingLives) { AsyncData(:final value) => (value == null || value.isEmpty) - ? FocusedOutlineButton( - autofocus: true, + ? LiveExploreErrorButton( + text: '팔로잉 채널이 없습니다', onPressed: () { ref .read( @@ -113,9 +113,6 @@ class LiveStreamFollowingList extends HookConsumerWidget { overlayType: OverlayType.following, ); }, - child: const CenterText( - text: '팔로잉 채널이 없습니다', - ), ) : ListView.builder( scrollDirection: Axis.horizontal, @@ -160,8 +157,8 @@ class LiveStreamFollowingList extends HookConsumerWidget { ); }, ), - AsyncError() => FocusedOutlineButton( - autofocus: true, + AsyncError() => LiveExploreErrorButton( + text: '팔로잉 채널을 불러오는 데 실패했습니다', onPressed: () { ref .read(controlOverlayTimerProvider.notifier) @@ -171,8 +168,6 @@ class LiveStreamFollowingList extends HookConsumerWidget { overlayType: OverlayType.following, ); }, - child: - const CenterText(text: '팔로잉 채널을 불러오는데 실패했습니다'), ), _ => const CenterText( text: '팔로잉 채널 불러오는 중...', diff --git a/lib/src/utils/video_player/widgets/live/popular/live_stream_popular_list.dart b/lib/src/utils/video_player/widgets/live/popular/live_stream_popular_list.dart index e0da890..3927ed4 100644 --- a/lib/src/utils/video_player/widgets/live/popular/live_stream_popular_list.dart +++ b/lib/src/utils/video_player/widgets/live/popular/live_stream_popular_list.dart @@ -2,7 +2,6 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:unofficial_chzzk_android_tv/src/common/widgets/focused_outline_button.dart'; import '../../../../../common/constants/dimensions.dart'; import '../../../../../common/constants/styles.dart'; @@ -15,6 +14,7 @@ import '../../../../focus/dpad_widget.dart'; import '../../../../popup/popup_utils.dart'; import '../../../controller/live_stream_controller.dart'; import '../../../controller/network_video_controller.dart'; +import '../../common/live_explore_error_button.dart'; class LiveStreamPopularList extends HookConsumerWidget { const LiveStreamPopularList({ @@ -31,7 +31,7 @@ class LiveStreamPopularList extends HookConsumerWidget { final focusScopeNode = useFocusScopeNode(); final scrollController = useScrollController(); - final asyncPopularLives = ref.watch(popularLiveControllerProvider); + final asyncPopularLives = ref.watch(popularLivesControllerProvider); useEffect(() { scrollController.addListener(() async { @@ -39,7 +39,7 @@ class LiveStreamPopularList extends HookConsumerWidget { if (scrollController.offset >= scrollController.position.maxScrollExtent - 50.0 && !scrollController.position.outOfRange) { - await ref.read(popularLiveControllerProvider.notifier).fetchMore(); + await ref.read(popularLivesControllerProvider.notifier).fetchMore(); } }); return null; @@ -113,7 +113,19 @@ class LiveStreamPopularList extends HookConsumerWidget { HomeBaseContainer( child: switch (asyncPopularLives) { AsyncData(:final value) => value == null - ? const CenterText(text: '인기 채널을 불러오는데 실패했습니다') + ? LiveExploreErrorButton( + text: '인기 채널을 불러오는 데 실패했습니다', + onPressed: () { + ref + .read( + controlOverlayTimerProvider.notifier) + .showOverlayAndStartTimer( + videoFocusNode: videoFocusNode, + seconds: 0, + overlayType: OverlayType.popular, + ); + }, + ) : ListView.builder( controller: scrollController, scrollDirection: Axis.horizontal, @@ -159,8 +171,8 @@ class LiveStreamPopularList extends HookConsumerWidget { ); }, ), - AsyncError() => FocusedOutlineButton( - autofocus: true, + AsyncError() => LiveExploreErrorButton( + text: '인기 채널을 불러오는 데 실패했습니다', onPressed: () { ref .read(controlOverlayTimerProvider.notifier) @@ -170,8 +182,6 @@ class LiveStreamPopularList extends HookConsumerWidget { overlayType: OverlayType.popular, ); }, - child: - const CenterText(text: '인기 채널을 불러오는데 실패했습니다'), ), _ => const CenterText(text: '인기 채널 불러오는 중...'), }, diff --git a/pubspec.lock b/pubspec.lock index 7fbab14..da3af6a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -273,6 +273,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "50fb435ed30c6d2525cbfaaa0f46851ea6131315f213c0d921b0e407b34e3b84" + url: "https://pub.dev" + source: hosted + version: "10.0.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: @@ -697,7 +713,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b @@ -1173,6 +1189,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6c1b342..b540dc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: chewie: ^1.7.5 hangul: ^0.6.0 gif: ^2.3.0 + path_provider: ^2.1.2 + device_info_plus: ^10.0.1 dev_dependencies: flutter_test: