Skip to content

Commit

Permalink
hotfix: virtual keyboard for webview login
Browse files Browse the repository at this point in the history
  • Loading branch information
Escaper-Park committed Mar 9, 2024
2 parents 7ab1032 + cd49bd6 commit 000326b
Show file tree
Hide file tree
Showing 25 changed files with 860 additions and 25 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
![동영상다시보기](./images/02.png)
![라이브탐색](./images/05.png)

## 핫픽스 - v0.2.1
- HeadlessWebView로 로그인이 진행되지 않는 분들을 위해 WebView 로그인을 추가했습니다. 가상 키보드에 문제가 있으신 분도 WebView 로그인을 사용해주세요. (설정 -> WebView 로그인)

## 패치노트 - v0.2.0
### 1 카테고리 검색, 라이브, 동영상, 홈 화면 즐겨찾기(로컬)
### 1. 카테고리 검색, 라이브, 동영상, 홈 화면 즐겨찾기(로컬)
- 홈 화면 즐겨찾기는 로컬 데이터로 저장되기 때문에 캐시를 삭제하면 초기화됩니다.
### 2. 동영상 전체 다시보기 추가
### 3. 채팅창 이모티콘 대응
Expand Down Expand Up @@ -49,15 +52,14 @@ APK 파일을 다운받아서 수동으로 설치합니다.
```

## 다운로드
[**APK 파일 다운로드 (v0.2.0)**](https://github.com/Escaper-Park/unofficial_chzzk_android_tv/releases/tag/v0.2.0)
[**APK 파일 다운로드 (v0.2.1)**](https://github.com/Escaper-Park/unofficial_chzzk_android_tv/releases/tag/v0.2.1)

### 설치 파일
- 사용하시는 티비의 CPU에 따라 설치하시면 됩니다.
- 설치가 되지 않는 다면 다른 버전으로 설치를 시도해보세요.
- 최적화 버전(저용량 파일들)이 설치되지 않는다면 통합 APK 파일로 설치를 시도해보세요. (chzzk-v0.2.0.apk)
- 사용하시는 CPU 타입에 따라 설치하시면 됩니다.
- 가능한 최적화 버전(v8a, v7a, x86)으로 설치해주세요. (설치 불가시 통합버전으로 설치)

## 현재 사용 가능한 기능
- 로그인 (Headless WebView)
- 로그인 (Headless WebView 또는 WebView)
- 팔로우 채널 목록
- 인기 채널 목록
- 채널 검색
Expand Down
3 changes: 3 additions & 0 deletions lib/src/common/constants/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class APIUrl {
static const String naverLogin =
'https://nid.naver.com/nidlogin.login?mode=form&url=https%3A%2F%2Fchzzk.naver.com%2F&locale=ko_KR&svctype=1';

static const String naverQRLogin =
'https://nid.naver.com/nidlogin.login?mode=qrcode&url=https%3A%2F%2Fchzzk.naver.com%2F&locale=ko_KR&svctype=1';

static String channel(String channelId) =>
'$_chzzkAPIUrl/service/v1/channels/$channelId';

Expand Down
3 changes: 3 additions & 0 deletions lib/src/common/constants/app_version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class AppVersion {
static const String version = 'v0.2.1';
}
5 changes: 3 additions & 2 deletions lib/src/features/auth/controller/auth_controller.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../multi_view/controller/multi_view_controller.dart';
import '../model/auth.dart';
import '../repository/auth_repository.dart';

Expand Down Expand Up @@ -47,8 +48,8 @@ class AuthController extends _$AuthController {
state = const AsyncValue.loading();

state = await AsyncValue.guard(() async {
// multiview channel
// ref.invalidate(multiViewControllerProvider);
// invalidate multiview channel
ref.invalidate(multiViewControllerProvider);

await ref.watch(authRepositoryProvider).deleteCookies();

Expand Down
2 changes: 1 addition & 1 deletion lib/src/features/auth/controller/auth_controller.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

243 changes: 243 additions & 0 deletions lib/src/features/auth/naver_login_with_webview_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../common/constants/api.dart';
import '../../common/widgets/base_scaffold.dart';
import '../../utils/router/app_router.dart';
import '../../utils/virtual_keyboard/controller/virtual_keyboard_input_controller.dart';
import '../../utils/virtual_keyboard/virtual_keyboard_input_display.dart';
import '../../utils/virtual_keyboard/virtual_keyboard_layout.dart';
import '../dashboard/controller/dashboard_controller.dart';
import './controller/auth_controller.dart';

enum LoginStep {
id,
password,
captcha,
}

class NaverLoginWithWebViewScreen extends StatefulHookConsumerWidget {
const NaverLoginWithWebViewScreen({super.key});

@override
ConsumerState<NaverLoginWithWebViewScreen> createState() =>
_NaverLoginDpadWebViewState();
}

class _NaverLoginDpadWebViewState
extends ConsumerState<NaverLoginWithWebViewScreen> {
InAppWebViewController? _controller;
InAppWebViewSettings settings = InAppWebViewSettings(
useHybridComposition: true,
javaScriptEnabled: true,
javaScriptCanOpenWindowsAutomatically: true,
thirdPartyCookiesEnabled: true,
cacheEnabled: true,
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
);

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
final webViewKey = useMemoized(GlobalKey.new, const []);
final loginStep = useState<LoginStep>(LoginStep.id);
final isObscure = useState<bool>(false);
final passwordStepCount = useState<int>(0);

final hintText = switch (loginStep.value) {
LoginStep.id => '아이디를 입력해주세요',
LoginStep.password => '비밀번호를 입력해주세요',
LoginStep.captcha => 'CAPTCHA 정답을 입력해주세요',
};

return PopScope(
canPop: false,
onPopInvoked: (_) {
ref.invalidate(authControllerProvider);

ref
.read(dashboardControllerProvider.notifier)
.changeScreen(context, AppRoute.home);
},
child: BaseScaffold(
verticalPadding: 0.0,
horizontalPadding: 0.0,
useTextField: true,
body: Row(
children: [
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(
top: 100.0,
left: 10.0,
right: 10.0,
),
child: Column(
children: [
SizedBox(
height: 100.0,
child: VirtualKeyboardInputDisplay(
headerText: hintText,
isObscure: isObscure.value,
),
),
Expanded(
child: VirtualKeyboardLayout(
onEnterPressed: (inputString) async {
String script = """ """;

if (inputString.isNotEmpty) {
switch (loginStep.value) {
case LoginStep.id:
script = """
var idField = document.getElementById('id');
if (idField.value === '') {
idField.value = '$inputString';
}
""";
loginStep.value = LoginStep.password;
isObscure.value = true;
ref
.read(virtualKeyboardInputControllerProvider
.notifier)
.reset();

if (_controller != null) {
await _controller!
.evaluateJavascript(source: script);
}
break;
case LoginStep.password:
if (passwordStepCount.value == 0) {
script = """
var passwordField = document.getElementById('pw');
if (passwordField.value === ''){
passwordField.value = '$inputString';
}
""";

if (_controller != null) {
await _controller!
.evaluateJavascript(source: script);
}

script = """
document.querySelector('[id="log.login"]').click();
""";

await Future.delayed(
const Duration(seconds: 1),
() {
_controller!
.evaluateJavascript(source: script);
},
);
loginStep.value = LoginStep.password;
isObscure.value = true;
passwordStepCount.value += 1;
} else {
script = """
var passwordField = document.getElementById('pw');
if (passwordField.value === ''){
passwordField.value = '$inputString';
}
var captchaField = document.getElementById('captcha');
captchaField.focus();
""";
loginStep.value = LoginStep.captcha;
isObscure.value = false;
ref
.read(
virtualKeyboardInputControllerProvider
.notifier)
.reset();
}

if (_controller != null) {
await _controller!
.evaluateJavascript(source: script);
}
break;
case LoginStep.captcha:
script = """
var captchaField = document.getElementById('captcha');
if (captchaField.value === ''){
captchaField.value = '$inputString';
}
""";

if (_controller != null) {
await _controller!
.evaluateJavascript(source: script);
}
script = """
document.querySelector('[id="log.login"]').click();
""";

await Future.delayed(
const Duration(seconds: 1),
() {
_controller!
.evaluateJavascript(source: script);
},
);
break;
}
}
},
),
),
],
),
),
),
Expanded(
flex: 1,
child: InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(url: WebUri(APIUrl.naverLogin)),
initialSettings: settings,
onWebViewCreated: (controller) {
_controller = controller;
},
onLoadStop: (controller, url) async {
if (loginStep.value == LoginStep.id) {
// Keep login
_controller!.evaluateJavascript(source: """
var smartLevel = document.getElementById('smart_LEVEL');
smartLevel.value = '-1';
var switchBlind = document.getElementById('switch_blind');
if (switchBlind != null) {
switchBlind.checked = false;
switchBlind.innerText = 'off';
}
var checkbox = document.querySelector('.input_keep');
if (checkbox !== null) {
checkbox.checked = true;
checkbox.value = 'on';
}
""");

await Future.delayed(const Duration(seconds: 1), () {
_controller!.scrollTo(x: 0, y: 0);
});
}
},
),
),
],
),
),
);
}
}
12 changes: 10 additions & 2 deletions lib/src/features/auth/widgets/login_text_form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,33 @@ import '../../../utils/focus/focus_utils.dart';
class LoginTextFormField extends HookWidget {
const LoginTextFormField({
super.key,
this.textEditingController,
required this.textInputFocusNode,
required this.buttonText,
required this.hintText,
required this.onPressed,
this.initText,
this.goBackAction,
this.isObscure = false,
this.autofocus = false,
this.enableSuggestion = false,
});

final TextEditingController? textEditingController;
final FocusNode textInputFocusNode;
final String buttonText;
final String hintText;
final bool isObscure;
final String? initText;
final VoidCallback? goBackAction;
final Function(String? text)? onPressed;
final bool autofocus;
final bool enableSuggestion;

@override
Widget build(BuildContext context) {
final controller = useTextEditingController(text: initText);
final controller =
textEditingController ?? useTextEditingController(text: initText);
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
useListenable(controller);

Expand Down Expand Up @@ -67,11 +74,12 @@ class LoginTextFormField extends HookWidget {
}
},
child: CustomTextFormField(
autofocus: false,
autofocus: autofocus,
focusNode: textInputFocusNode,
controller: controller,
isObscure: isObscure,
hintText: hintText,
enableSuggestion: enableSuggestion,
onFieldSubmitted: (_) {
FocusUtils.changeFocus(
currentFocus: textInputFocusNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ class _NaverLoginHeadlessWebViewState
void initState() {
super.initState();
headlessWebView = HeadlessInAppWebView(
initialUrlRequest: URLRequest(url: WebUri(APIUrl.naverLogin)),
initialUrlRequest: URLRequest(
url: WebUri(APIUrl.naverLogin),
),
initialSettings: InAppWebViewSettings(
useHybridComposition: true,
javaScriptEnabled: true,
javaScriptCanOpenWindowsAutomatically: true,
thirdPartyCookiesEnabled: true,
cacheEnabled: true,
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
),
onLoadStop: (controller, url) async {
await ref.read(settingsControllerProvider.notifier).setNaverIdPrefs(
Expand Down
Loading

0 comments on commit 000326b

Please sign in to comment.