Compare commits
58 Commits
d13769861d
...
4ec828b892
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec828b892 | |||
| 60677e262c | |||
| 598a04d084 | |||
| 9f27ea8aec | |||
| ba7d1ddee2 | |||
| 4a65902217 | |||
| 0f371ffd0e | |||
| 3287421614 | |||
| c0c5d6efc1 | |||
| 5bd4e45542 | |||
| 418b734c3f | |||
| 8e1dabbb80 | |||
| b4d6ef62a1 | |||
| 066d1dfe1a | |||
| 32f83a4612 | |||
| 43c112eb25 | |||
| 2b5240a565 | |||
| 93b620f4a8 | |||
| d8b2d53747 | |||
| d83c4b12ec | |||
| 2e700d4385 | |||
| 87bad6a959 | |||
| 0b3b4f8a1a | |||
| 5a70869dd8 | |||
| 2a44494d88 | |||
| 96108aa520 | |||
| de4b301ccb | |||
| 8153ad52ff | |||
| 4b2ef742d6 | |||
| 092fc67b0b | |||
| 5b83ae69dd | |||
| c74d27f4ab | |||
| 63a52629a9 | |||
| 80959abe16 | |||
| d048305193 | |||
| a78a6638da | |||
| 99f2715601 | |||
| e5f8d798d5 | |||
| d2ab5610c3 | |||
| 5e43411854 | |||
| 39c09ef8e5 | |||
| 8c7602bb1a | |||
| 1dcf16ba2a | |||
| 181eb28828 | |||
| ae66f80c3c | |||
| b32a3e5ea3 | |||
| 48f7bf631e | |||
| fc43022a95 | |||
| 9e867c3e16 | |||
| b62dba096b | |||
| 553f49a469 | |||
| deb0ce2482 | |||
| 21c87f95ef | |||
| 84803c171c | |||
| 94b48cef84 | |||
| 666424f79b | |||
| 9496a57b3c | |||
| ff1281abde |
@@ -7,7 +7,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
max_line_length = 130
|
||||
tab_width = 4
|
||||
|
||||
[*.{kt,kts}]
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,6 +45,7 @@ captures/
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/deviceManager.xml
|
||||
.idea/androidTestResultsUserPreferences.xml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
@@ -312,7 +313,6 @@ fabric.properties
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
docs/
|
||||
.junie/
|
||||
.kiro/
|
||||
|
||||
|
||||
6
.idea/junie.xml
generated
Normal file
6
.idea/junie.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JunieProject"><![CDATA[{
|
||||
"guidelinesPath": "AGENTS.md"
|
||||
}]]></component>
|
||||
</project>
|
||||
21
.opencode/commands/commit.md
Normal file
21
.opencode/commands/commit.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: commit-policy 스킬을 로드해 커밋 메시지 생성과 전후 검증을 수행한다
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
작업 목표:
|
||||
현재 변경사항을 안전하게 커밋한다.
|
||||
|
||||
필수 시작 단계:
|
||||
1. `skill` 도구로 `commit-policy` 스킬을 먼저 로드한다.
|
||||
- `skill({ name: "commit-policy" })`
|
||||
|
||||
실행 단계:
|
||||
1. 로드한 `commit-policy` 스킬의 Hard Requirements와 Execution Flow를 그대로 수행한다.
|
||||
2. `AGENTS.md`의 최소 정책(형식/한글 description/검증 스크립트)을 항상 만족한다.
|
||||
3. `$ARGUMENTS`가 있으면 scope 또는 description 의도에 반영하되, 스킬 규칙과 형식을 깨지 않는다.
|
||||
4. 마지막에 실행 명령과 pre-check/post-check PASS/FAIL 핵심 결과를 간단히 보고한다.
|
||||
|
||||
추가 사용자 의도:
|
||||
$ARGUMENTS
|
||||
46
.opencode/skills/commit-policy/SKILL.md
Normal file
46
.opencode/skills/commit-policy/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: commit-policy
|
||||
description: Apply this skill for any git commit task in this repository. It enforces commit message format and validation flow defined in AGENTS.md and work/scripts/check-commit-message-rules.sh, including pre-commit and post-commit verification.
|
||||
---
|
||||
|
||||
# Commit Policy Skill
|
||||
|
||||
Use this workflow whenever the task includes creating a commit.
|
||||
|
||||
## Required References
|
||||
|
||||
- `@AGENTS.md`
|
||||
- `@work/scripts/check-commit-message-rules.sh`
|
||||
|
||||
## Hard Requirements
|
||||
|
||||
1. Use commit subject format: `<type>(scope): <description>`.
|
||||
2. `type` must be lowercase (for example `feat`, `fix`, `chore`, `docs`, `refactor`, `test`).
|
||||
3. `description` must include Korean text and stay concise in imperative present tone.
|
||||
4. Optional footer must use `Refs: #123` or `Refs: #123, #456` format.
|
||||
5. Never commit secret files (`.env`, key/token/secret credential files).
|
||||
6. Never bypass hooks with `--no-verify`.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
1. Inspect context with:
|
||||
- `git status`
|
||||
- `git diff --cached`
|
||||
- `git diff`
|
||||
- `git log -5 --oneline`
|
||||
2. Stage commit target files only. Exclude suspicious secret-bearing files.
|
||||
3. Draft commit message from the change intent (focus on why, not only what).
|
||||
4. Run pre-commit validation with the full draft message:
|
||||
- `./work/scripts/check-commit-message-rules.sh --message "<full message>"`
|
||||
5. If validation fails, revise message and re-run until PASS.
|
||||
6. Commit using the validated message.
|
||||
7. Run post-commit validation:
|
||||
- `./work/scripts/check-commit-message-rules.sh`
|
||||
8. Report executed commands and PASS/FAIL summary.
|
||||
|
||||
## Output Checklist
|
||||
|
||||
- Final commit subject.
|
||||
- Whether pre-check passed.
|
||||
- Whether post-check passed.
|
||||
- Any excluded files and reason.
|
||||
180
AGENTS.md
180
AGENTS.md
@@ -1,18 +1,166 @@
|
||||
질문에 대한 답변과 설명은 한국어로 한다.
|
||||
# AGENTS.md
|
||||
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
|
||||
|
||||
## Quality Assurance Guidelines
|
||||
## 커뮤니케이션 규칙
|
||||
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||
|
||||
### Commit Standards
|
||||
1. Write in Korean.
|
||||
2. Use the present tense in the subject line (e.g., "Add feature" not "Added feature").
|
||||
3. Keep the subject line to 50 characters or less.
|
||||
4. Add a blank line between the subject and body.
|
||||
5. Keep the body to 72 characters or less per line.
|
||||
6. Within a paragraph, only break lines when the text exceeds 72 characters.
|
||||
7. Describe changes to public API features and do not include implementation details such as package-private code.
|
||||
8. Do not mention test code in commit messages.
|
||||
9. Do not use any prefix (such as "fix:", "update:", "docs:", "feat:", etc.) in the subject line.
|
||||
10. Do not start the subject line with a lowercase letter unless the first word is a function name or another identifier that is conventionally lowercase and there is a clear, justifiable reason for the exception. Otherwise, always start with an uppercase letter.
|
||||
11. Do not include tool advertisements, branding, or promotional content in commit messages.
|
||||
12. Use separate git commands to stage files before committing.
|
||||
13. Always validate commits using `work/scripts/check-commit-message-rules.sh` and fix until validation passes.
|
||||
## 저장소 범위
|
||||
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
||||
- 모든 명령은 저장소 루트에서 실행한다.
|
||||
- 추측하지 말고 근거 파일(`settings.gradle`, `build.gradle`, `app/build.gradle`, 소스 코드)을 읽고 결정한다.
|
||||
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
||||
|
||||
## 빌드 / 린트 / 테스트 명령
|
||||
기본 실행 형태:
|
||||
```bash
|
||||
./gradlew <task>
|
||||
```
|
||||
빌드:
|
||||
```bash
|
||||
./gradlew clean
|
||||
./gradlew :app:assembleDebug
|
||||
./gradlew :app:assembleRelease
|
||||
./gradlew :app:build
|
||||
./gradlew :app:check
|
||||
```
|
||||
린트/포맷:
|
||||
```bash
|
||||
./gradlew :app:lint
|
||||
./gradlew :app:lintDebug
|
||||
./gradlew :app:lintRelease
|
||||
./gradlew :app:ktlintCheck
|
||||
./gradlew :app:ktlintFormat
|
||||
```
|
||||
테스트:
|
||||
```bash
|
||||
./gradlew :app:test
|
||||
./gradlew :app:testDebugUnitTest
|
||||
./gradlew :app:testReleaseUnitTest
|
||||
./gradlew :app:connectedDebugAndroidTest
|
||||
```
|
||||
주의:
|
||||
- `:app:connectedDebugAndroidTest`는 기기/에뮬레이터 연결이 필요하다.
|
||||
- `app/build.gradle`에 `lint { checkReleaseBuilds false }`가 있어 릴리스 린트는 `:app:lintRelease`를 명시 실행해야 한다.
|
||||
- 현재 `app/src/androidTest`에는 테스트 소스가 없으므로 계측 테스트 명령은 신규 테스트 추가 시 사용한다.
|
||||
|
||||
### 1) 단일 테스트 실행 (중요)
|
||||
로컬 단위 테스트(`app/src/test`)는 `--tests` 필터를 사용한다.
|
||||
클래스 단위:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.chat.talk.room.ChatRepositoryTest"
|
||||
```
|
||||
메서드 단위:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.chat.talk.room.ChatRepositoryTest.enterChatRoom inserts messages and returns response"
|
||||
```
|
||||
패턴 매칭 예시:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "*TimeUtilsTest*"
|
||||
```
|
||||
참고:
|
||||
- Kotlin backtick 테스트명은 공백이 포함될 수 있으므로 전체 문자열을 인용한다.
|
||||
- 메서드 매칭이 불안정하면 클래스 단위로 먼저 실행한다.
|
||||
|
||||
### 2) 계측 테스트 클래스/메서드 타깃 실행
|
||||
Gradle 인자 방식:
|
||||
```bash
|
||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest
|
||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod
|
||||
```
|
||||
ADB 대안:
|
||||
```bash
|
||||
adb shell am instrument -w -e class kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod <test_package>/<runner>
|
||||
```
|
||||
|
||||
## 코드 스타일 가이드
|
||||
### 1) 포맷/기본 규칙
|
||||
- `.editorconfig` 기준을 준수한다.
|
||||
- 인덴트: 공백 4칸, 줄바꿈: LF, 최대 라인 길이: 130.
|
||||
- 파일 끝 개행 유지, trailing whitespace 제거.
|
||||
- Kotlin/KTS에서 `import-ordering` ktlint 규칙은 비활성화되어 있으므로 기존 파일 정렬 스타일을 우선 따른다.
|
||||
|
||||
### 2) import 규칙
|
||||
- 신규 코드에서는 와일드카드 import(`*`)를 기본적으로 지양한다.
|
||||
- 사용하지 않는 import를 남기지 않는다.
|
||||
- import alias(`as`)는 필요한 경우(이름 충돌 회피) 최소 범위로만 사용한다.
|
||||
- 기존 파일에 와일드카드/alias가 있으면 대규모 정렬 리팩터링 없이 주변 스타일에 맞춘다.
|
||||
|
||||
### 3) 네이밍/레이어
|
||||
- UI: `*Activity`, `*Fragment`, dialog/sheet suffix
|
||||
- 상태/도메인: `*ViewModel` (주로 `BaseViewModel` 상속)
|
||||
- 데이터 계층: `*Repository`, Retrofit `*Api`
|
||||
- DTO: `data class` + `*Request`, `*Response` suffix
|
||||
- 레이어 흐름: `Api` -> `Repository` -> `ViewModel` -> `Activity`/`Fragment`
|
||||
- DI는 `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`의 Koin 구성을 따른다.
|
||||
|
||||
### 4) 타입/계약/에러 처리
|
||||
- nullability와 제네릭 타입을 의미가 바뀌지 않게 유지한다.
|
||||
- 공개 API/스키마/리소스 계약은 요청 없이 변경하지 않는다.
|
||||
- 응답 처리 시 기존 `ApiResponse<T>`와 Rx 타입(`Single`, `Flowable`)을 우선 재사용한다.
|
||||
- 빈 `catch` 블록을 새로 추가하지 않는다.
|
||||
- 예외를 조용히 삼키지 않고 로그/주석/대체 흐름 중 하나를 남긴다.
|
||||
|
||||
### 5) 테스트 관례
|
||||
- 단위 테스트는 `app/src/test`에 위치하며 클래스명은 `*Test`를 사용한다.
|
||||
- 기본 스택은 JUnit4 + MockK/Mockito다.
|
||||
- 테스트 추가 시 단일 실행 명령 예시도 본 문서에 갱신한다.
|
||||
|
||||
## 커밋 메시지 규칙 (표준 Conventional Commits)
|
||||
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
|
||||
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
|
||||
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
|
||||
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test` 등)를 사용한다.
|
||||
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
|
||||
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
|
||||
|
||||
### 커밋 메시지 검증 절차
|
||||
- `git commit` 직전/직후 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
|
||||
- 스크립트 결과가 `[FAIL]`이면 메시지를 수정한 뒤 다시 검증한다.
|
||||
|
||||
## 작업 절차 체크리스트
|
||||
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
||||
- 변경 중: 공개 API 스키마를 임의 변경하지 말고 작은 단위로 안전하게 수정한다.
|
||||
- 변경 후: 최소 단일 테스트(`--tests`) 또는 `./gradlew :app:test`를 실행하고 필요 시 `./gradlew :app:ktlintCheck`를 수행한다.
|
||||
- 커밋 전/후: `commit-policy` 스킬을 먼저 로드하고, `git commit` 직전과 직후에 `work/scripts/check-commit-message-rules.sh`를 실행해 커밋 메시지 규칙 준수 여부를 확인한다.
|
||||
|
||||
## 작업 계획 문서 규칙 (docs)
|
||||
- 모든 작업 시작 전에 `docs` 폴더 아래 계획 문서를 먼저 생성하고, 해당 문서를 기준으로 구현한다.
|
||||
- 계획 문서 파일명은 `[날짜]_구현할내용한글.md` 형식을 사용한다.
|
||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||
- 구현 항목은 기능/작업 단위 체크박스(`- [ ]`)로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
||||
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||
- 결과 보고 시 문서 하단에 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)을 한국어로 남긴다.
|
||||
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰기 없이 누적한다.
|
||||
|
||||
## Cursor/Copilot 규칙 반영 현황
|
||||
- 확인 경로: `.cursor/rules/**`, `.cursorrules`, `.github/copilot-instructions.md`
|
||||
- 현재 저장소에는 위 파일이 존재하지 않는다.
|
||||
- 추후 규칙 파일이 추가되면 본 문서에 즉시 반영한다.
|
||||
|
||||
## 문서 유지보수 규칙
|
||||
- `build.gradle`/`app/build.gradle`/`settings.gradle` 변경 시 실행 명령 섹션을 함께 갱신한다.
|
||||
- 테스트 클래스 추가/이동 시 단일 테스트 실행 예시를 최신 상태로 유지한다.
|
||||
- `.editorconfig` 변경 시 포맷 규칙 섹션을 동기화한다.
|
||||
- 문서 변경 후 최소 한 번 `./gradlew tasks --all`로 명령 유효성을 확인한다.
|
||||
- 불확실한 규칙은 추측으로 채우지 말고 근거 파일 경로를 먼저 확인한다.
|
||||
- 에이전트 안내 문구는 한국어 중심으로 유지한다.
|
||||
|
||||
## 에이전트 동작 원칙
|
||||
- 추측하지 말고 근거 파일을 읽고 결정한다.
|
||||
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
||||
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
||||
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
|
||||
|
||||
## 설정/보안 유의사항
|
||||
- `local.properties`, 키스토어(`*.jks`, `*.keystore`, `*.p12`, `*.pem`, `*.key`)는 생성/수정 여부와 관계없이 커밋하지 않는다.
|
||||
- `app/src/debug/google-services.json`, `app/src/release/google-services.json`은 민감 구성으로 취급하고 외부 공유/로그 출력 금지한다.
|
||||
- `app/build.gradle`의 `buildConfigField` 값(토큰/앱키/시크릿 유사 값)은 신규 하드코딩을 추가하지 않는다.
|
||||
- `BuildConfig` 값(키/토큰/URL)을 로그, Toast, 크래시 메시지에 직접 노출하지 않는다.
|
||||
- 네트워크 로깅은 `AppDI.kt` 패턴을 유지한다(디버그만 BODY, 릴리스는 NONE).
|
||||
- 서명/배포 설정(Crashlytics, Google Services, Proguard, signing)은 요청 없이 변경하지 않는다.
|
||||
- `AndroidManifest.xml` 권한은 민감 영역이므로 신규 추가/확장은 사유와 영향도를 확인한 뒤 반영한다.
|
||||
- `applicationId`, `namespace`, OAuth Client ID, 딥링크 호스트는 요청 없이 변경하지 않는다.
|
||||
- 문서/이슈/PR 본문에 비밀값을 남기지 말고 필요 시 마스킹(`***`) 처리한다.
|
||||
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
|
||||
|
||||
@@ -63,8 +63,9 @@ android {
|
||||
applicationId "kr.co.vividnext.sodalive"
|
||||
minSdk 23
|
||||
targetSdk 35
|
||||
versionCode 218
|
||||
versionName "1.48.0"
|
||||
versionCode 225
|
||||
versionName "1.52.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -73,6 +74,9 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
|
||||
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"664c1707b18b225deca4b429"'
|
||||
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
|
||||
@@ -100,6 +104,9 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
|
||||
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
|
||||
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"667fca5d3bab7404f831c3e4"'
|
||||
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
|
||||
@@ -159,6 +166,7 @@ dependencies {
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||
}
|
||||
implementation "androidx.datastore:datastore-preferences:1.2.0"
|
||||
|
||||
// Gson
|
||||
implementation "com.google.code.gson:gson:2.13.2"
|
||||
@@ -247,6 +255,10 @@ dependencies {
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
|
||||
testImplementation 'io.mockk:mockk:1.14.6'
|
||||
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.AppPreferencesDataStoreProvider
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DataStoreRuntimeRegressionTest {
|
||||
|
||||
private val context: Context
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SharedPreferenceManager.init(context)
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SharedPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sharedPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
runBlocking {
|
||||
AppPreferencesDataStoreProvider.get(context).edit { preferences ->
|
||||
preferences[stringPreferencesKey(PREF_APP_LANGUAGE_CODE)] = "en"
|
||||
preferences[booleanPreferencesKey(Constants.PREF_IS_PLAYER_SERVICE_RUNNING)] = true
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferenceManager.resetForTest()
|
||||
SharedPreferenceManager.init(context)
|
||||
|
||||
assertEquals("en", SharedPreferenceManager.appLanguageCode)
|
||||
assertTrue(SharedPreferenceManager.isPlayerServiceRunning)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun chatRoomPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
val roomId = 13579L
|
||||
val visibleKey = "chat_bg_visible_room_$roomId"
|
||||
val imageIdKey = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
runBlocking {
|
||||
chatRoomDataStore().edit { preferences ->
|
||||
preferences[booleanPreferencesKey(visibleKey)] = false
|
||||
preferences[longPreferencesKey(imageIdKey)] = 777L
|
||||
}
|
||||
}
|
||||
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
|
||||
assertEquals(false, ChatRoomPreferenceManager.getBoolean(visibleKey, true))
|
||||
assertEquals(777L, ChatRoomPreferenceManager.getLong(imageIdKey, -1L))
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun chatRoomDataStore(): DataStore<Preferences> {
|
||||
val field = ChatRoomPreferenceManager::class.java.getDeclaredField("dataStore")
|
||||
field.isAccessible = true
|
||||
return field.get(ChatRoomPreferenceManager) as DataStore<Preferences>
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MiniPlayerConnectionGuardTest {
|
||||
|
||||
@Test
|
||||
fun mainActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { MainActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistDetailActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { AudioContentPlaylistDetailActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
private fun setPrivateField(target: Any, fieldName: String, value: Any?) {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
field.set(target, value)
|
||||
}
|
||||
|
||||
private fun getPrivateField(target: Any, fieldName: String): Any? {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
return field.get(target)
|
||||
}
|
||||
|
||||
private fun invokePrivateNoArg(target: Any, methodName: String) {
|
||||
val method = target.javaClass.getDeclaredMethod(methodName)
|
||||
method.isAccessible = true
|
||||
method.invoke(target)
|
||||
}
|
||||
|
||||
private fun <T> createOnMainThread(factory: () -> T): T {
|
||||
var instance: Any? = null
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
instance = factory()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return instance as T
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,7 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
|
||||
<activity android:name=".explorer.profile.UserProfileActivity" />
|
||||
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
|
||||
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />
|
||||
@@ -149,6 +150,7 @@
|
||||
<activity android:name=".settings.event.EventActivity" />
|
||||
<activity android:name=".settings.event.EventDetailActivity" />
|
||||
<activity android:name=".settings.notification.NotificationSettingsActivity" />
|
||||
<activity android:name=".settings.notification.NotificationReceiveSettingsActivity" />
|
||||
<activity android:name=".settings.ContentSettingsActivity" />
|
||||
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
|
||||
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
|
||||
@@ -166,6 +168,7 @@
|
||||
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
|
||||
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
|
||||
<activity android:name=".home.pushnotification.PushNotificationListActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
|
||||
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
|
||||
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface V2vApi {
|
||||
@POST("projects/{appId}/join")
|
||||
fun join(
|
||||
@Path("appId") appId: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Header("X-Request-Id") requestId: String,
|
||||
@Body request: V2vJoinRequest
|
||||
): Single<V2vJoinResponse>
|
||||
|
||||
@POST("projects/{appId}/agents/{agentId}/leave")
|
||||
fun leave(
|
||||
@Path("appId") appId: String,
|
||||
@Path("agentId") agentId: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Header("X-Request-Id") requestId: String
|
||||
): Single<V2vLeaveResponse>
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class V2vJoinRequest(
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("preset") val preset: String,
|
||||
@SerializedName("properties") val properties: V2vJoinProperties
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vJoinProperties(
|
||||
@SerializedName("channel") val channel: String,
|
||||
@SerializedName("token") val token: String,
|
||||
@SerializedName("agent_rtc_uid") val agentRtcUid: String,
|
||||
@SerializedName("remote_rtc_uids") val remoteRtcUids: List<String>,
|
||||
@SerializedName("idle_timeout") val idleTimeout: Int,
|
||||
@SerializedName("advanced_features") val advancedFeatures: V2vAdvancedFeatures,
|
||||
@SerializedName("parameters") val parameters: V2vParameters,
|
||||
@SerializedName("asr") val asr: V2vAsr,
|
||||
@SerializedName("translation") val translation: V2vTranslation,
|
||||
@SerializedName("tts") val tts: V2vTts
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vAdvancedFeatures(
|
||||
@SerializedName("enable_rtm") val enableRtm: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vParameters(
|
||||
@SerializedName("data_channel") val dataChannel: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vAsr(
|
||||
@SerializedName("language") val language: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vTranslation(
|
||||
@SerializedName("language") val language: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vTts(
|
||||
@SerializedName("enable") val enable: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vJoinResponse(
|
||||
@SerializedName("agent_id") val agentId: String,
|
||||
@SerializedName("create_ts") val createTs: Long,
|
||||
@SerializedName("status") val status: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vLeaveResponse(
|
||||
@SerializedName("agent_id") val agentId: String,
|
||||
@SerializedName("status") val status: String
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import android.util.Base64
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import java.util.UUID
|
||||
|
||||
class V2vRepository(private val api: V2vApi) {
|
||||
fun join(request: V2vJoinRequest): Single<V2vJoinResponse> {
|
||||
return api.join(
|
||||
appId = BuildConfig.AGORA_APP_ID,
|
||||
authorization = buildAuthorizationHeader(),
|
||||
requestId = generateRequestId(),
|
||||
request = request
|
||||
)
|
||||
}
|
||||
|
||||
fun leave(agentId: String): Single<V2vLeaveResponse> {
|
||||
return api.leave(
|
||||
appId = BuildConfig.AGORA_APP_ID,
|
||||
agentId = agentId,
|
||||
authorization = buildAuthorizationHeader(),
|
||||
requestId = generateRequestId()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildAuthorizationHeader(): String {
|
||||
val credentials = "${BuildConfig.AGORA_CUSTOMER_ID}:${BuildConfig.AGORA_CUSTOMER_SECRET}"
|
||||
val encoded = Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP)
|
||||
return "Basic $encoded"
|
||||
}
|
||||
|
||||
private fun generateRequestId(): String {
|
||||
return UUID.randomUUID().toString().replace("-", "")
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
@@ -40,6 +41,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
||||
|
||||
SodaLiveApplicationHolder.init(this)
|
||||
SharedPreferenceManager.init(applicationContext)
|
||||
ChatRoomPreferenceManager.init(applicationContext)
|
||||
|
||||
ImageLoaderProvider.init(applicationContext)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -13,6 +12,9 @@ import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
@@ -23,6 +25,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
||||
@@ -36,6 +40,9 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.random.Random
|
||||
|
||||
@UnstableApi
|
||||
@@ -60,25 +67,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
|
||||
private val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||
private var mediaController: MediaController? = null
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
handler.postDelayed(
|
||||
{
|
||||
initAndVisibleMiniPlayer()
|
||||
},
|
||||
1500
|
||||
)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||
private var playerStateJob: Job? = null
|
||||
|
||||
private fun initAndVisibleMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
@@ -94,32 +86,48 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
if (mediaController != null || mediaControllerFuture != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(applicationContext, componentName)
|
||||
val mediaControllerFuture =
|
||||
val controllerFuture =
|
||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
mediaControllerFuture = controllerFuture
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
try {
|
||||
if (mediaController != null) {
|
||||
controllerFuture.get().release()
|
||||
return@addListener
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
mediaController = controllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController?.isPlaying == true) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
it.play()
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Logger.e(throwable, "Failed to connect player service")
|
||||
} finally {
|
||||
mediaControllerFuture = null
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(applicationContext)
|
||||
@@ -163,7 +171,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
}
|
||||
|
||||
private fun deInitMiniPlayer() {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
mediaControllerFuture?.cancel(true)
|
||||
mediaControllerFuture = null
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
}
|
||||
@@ -180,18 +191,24 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
|
||||
bindData()
|
||||
viewModel.getPlaylistDetail(playlistId)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
initAndVisibleMiniPlayer()
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
playerStateJob = lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||
if (isRunning) {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
playerStateJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -44,12 +44,13 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
bindData()
|
||||
|
||||
viewModel.seriesId = seriesId
|
||||
viewModel.getSeriesDetail()
|
||||
viewModel.getSeriesDetail { finish() }
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
|
||||
@@ -34,7 +34,7 @@ class SeriesDetailViewModel(
|
||||
private val unknownErrorMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
|
||||
fun getSeriesDetail() {
|
||||
fun getSeriesDetail(onFailure: (() -> Unit)? = null) {
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
@@ -51,6 +51,10 @@ class SeriesDetailViewModel(
|
||||
_seriesDetailLiveData.value = seriesDetailResponse
|
||||
} else {
|
||||
_toastLiveData.value = it.message ?: unknownErrorMessage
|
||||
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
},
|
||||
@@ -58,6 +62,10 @@ class SeriesDetailViewModel(
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -41,7 +39,6 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
private var characterId: Long = 0L
|
||||
private var profileUrl: String = ""
|
||||
|
||||
private val prefsName = "chat_room_prefs"
|
||||
private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
private lateinit var adapter: BgAdapter
|
||||
@@ -87,8 +84,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
|
||||
private fun loadData() {
|
||||
// 초기 선택: 저장된 이미지 ID 사용
|
||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
val savedId = prefs.getLong(bgImageIdKey(roomId), -1L)
|
||||
val savedId = ChatRoomPreferenceManager.getLong(bgImageIdKey(roomId), -1L)
|
||||
selectedId = if (savedId > 0) savedId else null
|
||||
items.clear()
|
||||
|
||||
@@ -124,12 +120,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun saveAndApply(item: BgItem) {
|
||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
prefs.edit {
|
||||
if (item.id > 0) putLong(
|
||||
bgImageIdKey(roomId),
|
||||
item.id
|
||||
) else remove(bgImageIdKey(roomId))
|
||||
if (item.id > 0) {
|
||||
ChatRoomPreferenceManager.putLong(bgImageIdKey(roomId), item.id)
|
||||
} else {
|
||||
ChatRoomPreferenceManager.remove(bgImageIdKey(roomId))
|
||||
}
|
||||
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -55,12 +54,10 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
||||
private var characterInfo: CharacterInfo? = null
|
||||
|
||||
// 5.4 SharedPreferences (안내 메시지 접힘 상태 저장)
|
||||
private val prefs by lazy { getSharedPreferences("chat_room_prefs", MODE_PRIVATE) }
|
||||
private fun noticePrefKey(roomId: Long) = "chat_notice_hidden_room_${roomId}"
|
||||
private fun isNoticeHidden(): Boolean = prefs.getBoolean(noticePrefKey(roomId), false)
|
||||
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
||||
private fun setNoticeHidden(hidden: Boolean) {
|
||||
prefs.edit { putBoolean(noticePrefKey(roomId), hidden) }
|
||||
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
@@ -866,7 +863,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
}
|
||||
|
||||
fun applyBackgroundVisibility() {
|
||||
val visible = prefs.getBoolean("chat_bg_visible_room_$roomId", true)
|
||||
val visible = ChatRoomPreferenceManager.getBoolean("chat_bg_visible_room_$roomId", true)
|
||||
binding.ivBackgroundProfile.isVisible = visible
|
||||
binding.viewCharacterDim.isVisible = visible
|
||||
}
|
||||
@@ -885,7 +882,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
private fun bgImageIdPrefKey(): String = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
private fun getSavedBackgroundImageId(): Long? {
|
||||
val id = prefs.getLong(bgImageIdPrefKey(), -1L)
|
||||
val id = ChatRoomPreferenceManager.getLong(bgImageIdPrefKey(), -1L)
|
||||
return if (id > 0) id else null
|
||||
}
|
||||
|
||||
@@ -912,9 +909,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
"chat_bg_image_id_room_$roomId",
|
||||
noticePrefKey(roomId)
|
||||
)
|
||||
prefs.edit {
|
||||
keys.forEach { remove(it) }
|
||||
}
|
||||
ChatRoomPreferenceManager.removeAll(keys)
|
||||
}
|
||||
|
||||
fun onResetChatRequested() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,7 +10,6 @@ import android.widget.RelativeLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* 채팅방 우측 상단 더보기 버튼 클릭 시 표시되는 전체화면 다이얼로그.
|
||||
@@ -42,13 +40,12 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
|
||||
// 닫기 버튼
|
||||
view.findViewById<ImageView>(R.id.iv_close)?.setOnClickListener { dismiss() }
|
||||
|
||||
val prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val bgKey = bgPrefKey(roomId)
|
||||
|
||||
val switch = view.findViewById<SwitchMaterial>(R.id.sw_background)
|
||||
switch?.isChecked = prefs.getBoolean(bgKey, true)
|
||||
switch?.isChecked = ChatRoomPreferenceManager.getBoolean(bgKey, true)
|
||||
switch?.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit { putBoolean(bgKey, isChecked) }
|
||||
ChatRoomPreferenceManager.putBoolean(bgKey, isChecked)
|
||||
(activity as? ChatRoomActivity)?.applyBackgroundVisibility()
|
||||
}
|
||||
|
||||
@@ -76,7 +73,6 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_ROOM_ID = "arg_room_id"
|
||||
private const val PREFS_NAME = "chat_room_prefs"
|
||||
private fun bgPrefKey(roomId: Long) = "chat_bg_visible_room_$roomId"
|
||||
|
||||
fun newInstance(roomId: Long): ChatRoomMoreDialogFragment {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object ChatRoomPreferenceManager {
|
||||
private const val LEGACY_PREFERENCES_NAME = "chat_room_prefs"
|
||||
private const val DATASTORE_FILE_NAME = "chat_room_preferences"
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val preferenceState = MutableStateFlow<Map<String, Any?>>(emptyMap())
|
||||
private val initLock = Any()
|
||||
private var observerJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
fun init(context: Context) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
val appContext = context.applicationContext
|
||||
if (!this::dataStore.isInitialized) {
|
||||
dataStore = androidx.datastore.preferences.core.PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(appContext, LEGACY_PREFERENCES_NAME)),
|
||||
produceFile = { appContext.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
val initialPreferences = runBlocking {
|
||||
dataStore.data.first()
|
||||
}
|
||||
updateState(initialPreferences)
|
||||
initialized = true
|
||||
|
||||
observerJob?.cancel()
|
||||
observerJob = appScope.launch {
|
||||
dataStore.data.collect { preferences ->
|
||||
updateState(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(preferences: Preferences) {
|
||||
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
|
||||
key.name to value
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(initLock) {
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
preferenceState.value = emptyMap()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? Boolean ?: defaultValue
|
||||
}
|
||||
|
||||
fun putBoolean(key: String, value: Boolean) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLong(key: String, defaultValue: Long): Long {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? Long ?: defaultValue
|
||||
}
|
||||
|
||||
fun putLong(key: String, value: Long) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[longPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? String ?: defaultValue
|
||||
}
|
||||
|
||||
fun putString(key: String, value: String) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable.remove(key)
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val prefKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return@edit
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll(keys: Collection<String>) {
|
||||
ensureInitialized()
|
||||
if (keys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
keys.forEach { key ->
|
||||
mutable.remove(key)
|
||||
}
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val mapKeys = preferences.asMap().keys
|
||||
keys.forEach { keyName ->
|
||||
val prefKey = mapKeys.firstOrNull { it.name == keyName } ?: return@forEach
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
check(initialized) { "ChatRoomPreferenceManager is not initialized." }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
|
||||
object AppPreferencesDataStoreProvider {
|
||||
private const val DATASTORE_FILE_NAME = "sodalive_default_preferences"
|
||||
private const val DEFAULT_SHARED_PREFERENCES_SUFFIX = "_preferences"
|
||||
|
||||
@Volatile
|
||||
private var dataStore: DataStore<Preferences>? = null
|
||||
|
||||
fun get(context: Context): DataStore<Preferences> {
|
||||
val existing = dataStore
|
||||
if (existing != null) {
|
||||
return existing
|
||||
}
|
||||
|
||||
return synchronized(this) {
|
||||
dataStore ?: createDataStore(context.applicationContext).also { dataStore = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDataStore(context: Context): DataStore<Preferences> {
|
||||
val legacyPreferencesName = "${context.packageName}$DEFAULT_SHARED_PREFERENCES_SUFFIX"
|
||||
return PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(
|
||||
// 기존 기본 SharedPreferences 값은 DataStore 첫 접근 시 자동 이관된다.
|
||||
SharedPreferencesMigration(context, legacyPreferencesName)
|
||||
),
|
||||
produceFile = { context.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(this) {
|
||||
dataStore = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ object Constants {
|
||||
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
|
||||
const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver"
|
||||
const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver"
|
||||
const val ACTION_LIVE_ROOM_DEEPLINK_CONFIRM = "soda_live_action_live_room_deeplink_confirm"
|
||||
|
||||
const val EXTRA_COMMUNITY_POST_ID = "community_post_id"
|
||||
const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id"
|
||||
|
||||
@@ -1,167 +1,270 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object SharedPreferenceManager {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val preferenceState = MutableStateFlow<Map<String, Any?>>(emptyMap())
|
||||
private val initLock = Any()
|
||||
private var observerJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
val roleFlow: Flow<String> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_USER_ROLE] as? String ?: MemberRole.USER.name
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val isPlayerServiceRunningFlow: Flow<Boolean> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] as? Boolean ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun init(context: Context) {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun registerOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
dataStore = AppPreferencesDataStoreProvider.get(context.applicationContext)
|
||||
val initialPreferences = runBlocking {
|
||||
dataStore.data.first()
|
||||
}
|
||||
updateState(initialPreferences)
|
||||
initialized = true
|
||||
|
||||
fun clear() {
|
||||
sharedPreferences.edit { editor ->
|
||||
sharedPreferences.all.keys
|
||||
.filterNot { it == Constants.PREF_PUSH_TOKEN }
|
||||
.forEach { editor.remove(it) }
|
||||
observerJob?.cancel()
|
||||
observerJob = appScope.launch {
|
||||
dataStore.data.collect { preferences ->
|
||||
updateState(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
|
||||
val editor = this.edit()
|
||||
operation(editor)
|
||||
editor.apply()
|
||||
private fun updateState(preferences: Preferences) {
|
||||
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
|
||||
key.name to value
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun SharedPreferences.set(key: String, value: Any?) {
|
||||
when (value) {
|
||||
is String? -> edit { it.putString(key, value) }
|
||||
is Int -> edit { it.putInt(key, value) }
|
||||
is Boolean -> edit { it.putBoolean(key, value) }
|
||||
is Float -> edit { it.putFloat(key, value) }
|
||||
is Long -> edit { it.putLong(key, value) }
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(initLock) {
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
preferenceState.value = emptyMap()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val pushToken = state[Constants.PREF_PUSH_TOKEN]
|
||||
if (pushToken != null) {
|
||||
mapOf(Constants.PREF_PUSH_TOKEN to pushToken)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val keysToRemove = preferences.asMap().keys.filter { it.name != Constants.PREF_PUSH_TOKEN }
|
||||
keysToRemove.forEach { preferenceKey ->
|
||||
preferences.remove(preferenceKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
check(initialized) { "SharedPreferenceManager is not initialized." }
|
||||
}
|
||||
|
||||
private fun setPreference(key: String, value: Any?) {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
if (value == null) {
|
||||
mutable.remove(key)
|
||||
} else {
|
||||
mutable[key] = value
|
||||
}
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
when (value) {
|
||||
null -> removeByName(preferences, key)
|
||||
is String -> preferences[stringPreferencesKey(key)] = value
|
||||
is Int -> preferences[intPreferencesKey(key)] = value
|
||||
is Boolean -> preferences[booleanPreferencesKey(key)] = value
|
||||
is Float -> preferences[floatPreferencesKey(key)] = value
|
||||
is Long -> preferences[longPreferencesKey(key)] = value
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private operator fun <T> SharedPreferences.get(key: String, defaultValue: T? = null): T {
|
||||
private fun <T> getPreference(key: String, defaultValue: T? = null): T {
|
||||
ensureInitialized()
|
||||
val value = preferenceState.value[key]
|
||||
|
||||
return when (defaultValue) {
|
||||
is String, null -> getString(key, defaultValue as? String) as T
|
||||
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
|
||||
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
|
||||
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
|
||||
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
|
||||
is String, null -> (value as? String ?: defaultValue as? String) as T
|
||||
is Int -> (value as? Int ?: defaultValue) as T
|
||||
is Boolean -> (value as? Boolean ?: defaultValue) as T
|
||||
is Float -> (value as? Float ?: defaultValue) as T
|
||||
is Long -> (value as? Long ?: defaultValue) as T
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeByName(preferences: MutablePreferences, key: String) {
|
||||
val targetKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return
|
||||
preferences.remove(targetKey)
|
||||
}
|
||||
|
||||
var token: String
|
||||
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_TOKEN] = value
|
||||
setPreference(Constants.PREF_TOKEN, value)
|
||||
}
|
||||
|
||||
var userId: Long
|
||||
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
|
||||
get() = getPreference(Constants.PREF_USER_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ID] = value
|
||||
setPreference(Constants.PREF_USER_ID, value)
|
||||
}
|
||||
|
||||
var nickname: String
|
||||
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
|
||||
get() = getPreference(Constants.PREF_NICKNAME, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NICKNAME] = value
|
||||
setPreference(Constants.PREF_NICKNAME, value)
|
||||
}
|
||||
|
||||
var email: String
|
||||
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
|
||||
get() = getPreference(Constants.PREF_EMAIL, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_EMAIL] = value
|
||||
setPreference(Constants.PREF_EMAIL, value)
|
||||
}
|
||||
|
||||
var profileImage: String
|
||||
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
|
||||
get() = getPreference(Constants.PREF_PROFILE_IMAGE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
|
||||
setPreference(Constants.PREF_PROFILE_IMAGE, value)
|
||||
}
|
||||
|
||||
var can: Int
|
||||
get() = sharedPreferences[Constants.PREF_CAN, 0]
|
||||
get() = getPreference(Constants.PREF_CAN, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CAN] = value
|
||||
setPreference(Constants.PREF_CAN, value)
|
||||
}
|
||||
|
||||
var point: Int
|
||||
get() = sharedPreferences[Constants.PREF_POINT, 0]
|
||||
get() = getPreference(Constants.PREF_POINT, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_POINT] = value
|
||||
setPreference(Constants.PREF_POINT, value)
|
||||
}
|
||||
|
||||
var role: String
|
||||
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
|
||||
get() = getPreference(Constants.PREF_USER_ROLE, MemberRole.USER.name)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ROLE] = value
|
||||
setPreference(Constants.PREF_USER_ROLE, value)
|
||||
}
|
||||
|
||||
var isAuth: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT] = value
|
||||
setPreference(Constants.PREF_IS_ADULT, value)
|
||||
}
|
||||
|
||||
var isAuditionNotification: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION, false]
|
||||
get() = getPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION] = value
|
||||
setPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, value)
|
||||
}
|
||||
|
||||
var isAdultContentVisible: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE] = value
|
||||
setPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, value)
|
||||
}
|
||||
|
||||
var contentPreference: Int
|
||||
get() = sharedPreferences[Constants.PREF_CONTENT_PREFERENCE, 0]
|
||||
get() = getPreference(Constants.PREF_CONTENT_PREFERENCE, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CONTENT_PREFERENCE] = value
|
||||
setPreference(Constants.PREF_CONTENT_PREFERENCE, value)
|
||||
}
|
||||
|
||||
var pushToken: String
|
||||
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_PUSH_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
|
||||
setPreference(Constants.PREF_PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
var isContentPlayLoop: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
|
||||
get() = getPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
|
||||
setPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, value)
|
||||
}
|
||||
|
||||
var notShowingEventPopupId: Long
|
||||
get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0]
|
||||
get() = getPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
|
||||
setPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, value)
|
||||
}
|
||||
|
||||
var isViewedOnboardingTutorial: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false]
|
||||
get() = getPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
|
||||
setPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, value)
|
||||
}
|
||||
|
||||
var noChatRoomList: List<Long>
|
||||
get() {
|
||||
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
|
||||
val list = getPreference(Constants.PREF_NO_CHAT_ROOM, "")
|
||||
val gson = Gson()
|
||||
val listType = object : TypeToken<List<Long>>() {}.type
|
||||
val myList = gson.fromJson<List<Long>>(list, listType)
|
||||
@@ -170,54 +273,60 @@ object SharedPreferenceManager {
|
||||
set(value) {
|
||||
val gson = Gson()
|
||||
val listJson = gson.toJson(value)
|
||||
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
|
||||
setPreference(Constants.PREF_NO_CHAT_ROOM, listJson)
|
||||
}
|
||||
|
||||
var isPlayerServiceRunning: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false]
|
||||
get() = getPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] = value
|
||||
setPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, value)
|
||||
}
|
||||
|
||||
var marketingPid: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_PID, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_PID, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_PID] = value
|
||||
setPreference(Constants.PREF_MARKETING_PID, value)
|
||||
}
|
||||
|
||||
var marketingUtmSource: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_SOURCE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_SOURCE, value)
|
||||
}
|
||||
|
||||
var marketingUtmMedium: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_MEDIUM, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_MEDIUM, value)
|
||||
}
|
||||
|
||||
var marketingUtmCampaign: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, value)
|
||||
}
|
||||
|
||||
var marketingLinkValue: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE, value)
|
||||
}
|
||||
|
||||
var marketingLinkValueId: Long
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID, 0L]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, value)
|
||||
}
|
||||
|
||||
var alreadyTrackingAppLaunch: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true]
|
||||
get() = getPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH] = value
|
||||
setPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, value)
|
||||
}
|
||||
|
||||
var appLanguageCode: String
|
||||
get() = getPreference(PREF_APP_LANGUAGE_CODE, "")
|
||||
set(value) {
|
||||
setPreference(PREF_APP_LANGUAGE_CODE, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.di
|
||||
import android.content.Context
|
||||
import com.google.gson.GsonBuilder
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vApi
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
|
||||
@@ -69,6 +71,7 @@ import kr.co.vividnext.sodalive.common.ApiBuilder
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerApi
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAllViewModel
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityApi
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
|
||||
@@ -84,6 +87,7 @@ import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
|
||||
import kr.co.vividnext.sodalive.home.HomeApi
|
||||
import kr.co.vividnext.sodalive.home.HomeRepository
|
||||
import kr.co.vividnext.sodalive.home.HomeViewModel
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListViewModel
|
||||
import kr.co.vividnext.sodalive.live.LiveApi
|
||||
import kr.co.vividnext.sodalive.live.LiveRepository
|
||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||
@@ -156,6 +160,7 @@ import kr.co.vividnext.sodalive.settings.event.EventViewModel
|
||||
import kr.co.vividnext.sodalive.settings.notice.NoticeApi
|
||||
import kr.co.vividnext.sodalive.settings.notice.NoticeRepository
|
||||
import kr.co.vividnext.sodalive.settings.notice.NoticeViewModel
|
||||
import kr.co.vividnext.sodalive.settings.notification.NotificationReceiveSettingsViewModel
|
||||
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsViewModel
|
||||
import kr.co.vividnext.sodalive.settings.signout.SignOutViewModel
|
||||
import kr.co.vividnext.sodalive.settings.terms.TermsApi
|
||||
@@ -176,6 +181,7 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
|
||||
@@ -184,6 +190,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
private val baseUrl = BuildConfig.BASE_URL
|
||||
private val agoraBaseUrl = "https://api.agora.io/api/speech-to-speech-translation/v2/"
|
||||
|
||||
private val otherModule = module {
|
||||
single { GsonBuilder().create() }
|
||||
@@ -211,6 +218,23 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
.build()
|
||||
}
|
||||
|
||||
single<OkHttpClient>(named("agoraHttpClient")) {
|
||||
val logging = HttpLoggingInterceptor()
|
||||
|
||||
if (isDebugMode) {
|
||||
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
} else {
|
||||
logging.setLevel(HttpLoggingInterceptor.Level.NONE)
|
||||
}
|
||||
|
||||
OkHttpClient().newBuilder()
|
||||
.addInterceptor(logging)
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
single { AcceptLanguageInterceptor(get()) }
|
||||
|
||||
single {
|
||||
@@ -222,6 +246,15 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
.build()
|
||||
}
|
||||
|
||||
single<Retrofit>(named("agoraRetrofit")) {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(agoraBaseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.client(get<OkHttpClient>(named("agoraHttpClient")))
|
||||
.build()
|
||||
}
|
||||
|
||||
single { ApiBuilder().build(get(), AlarmListApi::class.java) }
|
||||
single { ApiBuilder().build(get(), CanApi::class.java) }
|
||||
single { ApiBuilder().build(get(), CanTempApi::class.java) }
|
||||
@@ -255,6 +288,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
single { ApiBuilder().build(get(), TalkApi::class.java) }
|
||||
single { ApiBuilder().build(get(), CharacterCommentApi::class.java) }
|
||||
single { ApiBuilder().build(get(), OriginalWorkApi::class.java) }
|
||||
single { ApiBuilder().build(get<Retrofit>(named("agoraRetrofit")), V2vApi::class.java) }
|
||||
}
|
||||
|
||||
private val viewModelModule = module {
|
||||
@@ -273,10 +307,20 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
viewModel { LiveRoomCreateViewModel(get()) }
|
||||
viewModel { LiveTagViewModel(get()) }
|
||||
viewModel { LiveRoomEditViewModel(get()) }
|
||||
viewModel { LiveRoomViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel {
|
||||
LiveRoomViewModel(
|
||||
repository = get(),
|
||||
userRepository = get(),
|
||||
reportRepository = get(),
|
||||
rouletteRepository = get(),
|
||||
userEventRepository = get(),
|
||||
v2vRepository = get()
|
||||
)
|
||||
}
|
||||
viewModel { LiveRoomDonationMessageViewModel(get()) }
|
||||
viewModel { ExplorerViewModel(get()) }
|
||||
viewModel { UserProfileViewModel(get(), get(), get()) }
|
||||
viewModel { UserProfileChannelDonationAllViewModel(get()) }
|
||||
viewModel { UserFollowerListViewModel(get(), get()) }
|
||||
viewModel { TextMessageViewModel(get()) }
|
||||
viewModel { TextMessageWriteViewModel(get()) }
|
||||
@@ -286,6 +330,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
viewModel { NoticeViewModel(get()) }
|
||||
viewModel { EventViewModel(get()) }
|
||||
viewModel { NotificationSettingsViewModel(get()) }
|
||||
viewModel { NotificationReceiveSettingsViewModel(get(), get()) }
|
||||
viewModel { ContentSettingsViewModel() }
|
||||
viewModel { SettingsViewModel(get(), get()) }
|
||||
viewModel { SeriesDetailViewModel(get(), get()) }
|
||||
@@ -334,6 +379,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
viewModel { SearchViewModel(get()) }
|
||||
viewModel { PointStatusViewModel(get()) }
|
||||
viewModel { HomeViewModel(get(), get()) }
|
||||
viewModel { PushNotificationListViewModel(get()) }
|
||||
viewModel { CharacterTabViewModel(get()) }
|
||||
viewModel { CharacterDetailViewModel(get()) }
|
||||
viewModel { CharacterGalleryViewModel(get()) }
|
||||
@@ -353,6 +399,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
factory { TermsRepository(get()) }
|
||||
factory { SeriesRepository(get()) }
|
||||
factory { LiveRepository(get(), get(), get()) }
|
||||
factory { V2vRepository(get()) }
|
||||
factory { EventRepository(get()) }
|
||||
factory { LiveRecommendRepository(get()) }
|
||||
factory { AuthRepository(get()) }
|
||||
@@ -392,7 +439,6 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
factory { SeriesMainRepository(get()) }
|
||||
}
|
||||
|
||||
|
||||
private val moduleList = listOf(
|
||||
networkModule,
|
||||
viewModelModule,
|
||||
|
||||
@@ -126,20 +126,9 @@ class MemberProfileDialog(
|
||||
|
||||
private fun showMemberBlockDialog(memberId: Long, nickname: String) {
|
||||
val message = if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
"""
|
||||
${nickname}님을 차단하시겠습니까?
|
||||
|
||||
사용자를 차단하면 사용자는 아래 기능이 제한됩니다.
|
||||
- 내가 개설한 라이브 입장 불가
|
||||
- 나에게 메시지 보내기 불가
|
||||
- 내 채널의 팬Talk 작성불가
|
||||
""".trimIndent()
|
||||
SodaLiveApplicationHolder.get().getString(R.string.screen_live_room_block_message_creator, nickname)
|
||||
} else {
|
||||
"""
|
||||
${nickname}님을 차단하시겠습니까?
|
||||
|
||||
- 사용자를 차단하면 '차단한 사용자의 라이브 중 채팅'이 보이지 않습니다.
|
||||
""".trimIndent()
|
||||
SodaLiveApplicationHolder.get().getString(R.string.screen_live_room_block_message_user, nickname)
|
||||
}
|
||||
val dialog = android.app.AlertDialog.Builder(activity)
|
||||
dialog.setTitle(
|
||||
|
||||
@@ -5,8 +5,11 @@ import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
|
||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||
@@ -43,11 +46,18 @@ interface ExplorerApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetCreatorProfileResponse>>
|
||||
|
||||
@GET("/explorer/profile/{id}/detail")
|
||||
fun getCreatorDetail(
|
||||
@Path("id") id: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetCreatorDetailResponse>>
|
||||
|
||||
@GET("/explorer/profile/{id}/donation-rank")
|
||||
fun getCreatorProfileDonationRanking(
|
||||
@Path("id") id: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("period") period: String?,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetDonationAllResponse>>
|
||||
|
||||
@@ -72,6 +82,18 @@ interface ExplorerApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@POST("/explorer/profile/channel-donation")
|
||||
fun postChannelDonation(
|
||||
@Body request: PostChannelDonationRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@GET("/explorer/profile/channel-donation")
|
||||
fun getChannelDonationList(
|
||||
@Query("creatorId") creatorId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetChannelDonationListResponse>>
|
||||
|
||||
@GET("/explorer/profile/{id}/follower-list")
|
||||
fun getFollowerList(
|
||||
@Path("id") userId: Long,
|
||||
|
||||
@@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
|
||||
import java.util.TimeZone
|
||||
|
||||
@@ -27,6 +29,11 @@ class ExplorerRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCreatorDetail(id: Long, token: String) = api.getCreatorDetail(
|
||||
id = id,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCreatorProfileCheers(
|
||||
creatorId: Long,
|
||||
page: Int,
|
||||
@@ -64,6 +71,22 @@ class ExplorerRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun postChannelDonation(
|
||||
request: PostChannelDonationRequest,
|
||||
token: String
|
||||
) = api.postChannelDonation(
|
||||
request = request,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getChannelDonationList(
|
||||
creatorId: Long,
|
||||
token: String
|
||||
): Single<ApiResponse<GetChannelDonationListResponse>> = api.getChannelDonationList(
|
||||
creatorId = creatorId,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getFollowerList(
|
||||
userId: Long,
|
||||
page: Int,
|
||||
@@ -80,12 +103,14 @@ class ExplorerRepository(
|
||||
id: Long,
|
||||
page: Int,
|
||||
size: Int,
|
||||
donationRankingPeriod: String?,
|
||||
token: String
|
||||
): Single<ApiResponse<GetDonationAllResponse>> {
|
||||
return api.getCreatorProfileDonationRanking(
|
||||
id = id,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
period = donationRankingPeriod,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.explorer.profile
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListItem
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
|
||||
|
||||
@Keep
|
||||
@@ -25,10 +26,10 @@ data class GetCreatorProfileResponse(
|
||||
val notice: String,
|
||||
@SerializedName("communityPostList")
|
||||
val communityPostList: List<GetCommunityPostListResponse>,
|
||||
@SerializedName("channelDonationList")
|
||||
val channelDonationList: List<GetChannelDonationListItem>,
|
||||
@SerializedName("cheers")
|
||||
val cheers: GetCheersResponse,
|
||||
@SerializedName("activitySummary")
|
||||
val activitySummary: GetCreatorActivitySummary,
|
||||
@SerializedName("seriesList")
|
||||
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
|
||||
@SerializedName("isBlock")
|
||||
@@ -46,8 +47,9 @@ data class CreatorResponse(
|
||||
@SerializedName("introduce") val introduce: String = "",
|
||||
@SerializedName("instagramUrl") val instagramUrl: String? = null,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String? = null,
|
||||
@SerializedName("blogUrl") val blogUrl: String? = null,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String? = null,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String? = null,
|
||||
@SerializedName("xUrl") val xUrl: String? = null,
|
||||
@SerializedName("isAvailableChat") val isAvailableChat: Boolean = true,
|
||||
@SerializedName("isFollow") val isFollow: Boolean,
|
||||
@SerializedName("isNotify") val isNotify: Boolean,
|
||||
@@ -112,5 +114,3 @@ data class GetCreatorActivitySummary(
|
||||
@SerializedName("liveContributorCount") val liveContributorCount: Int,
|
||||
@SerializedName("contentCount") val contentCount: Int
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,11 +46,15 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
|
||||
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListItem
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAdapter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.relativeTimeText
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.detail.CreatorDetailDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
|
||||
@@ -60,18 +64,21 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
|
||||
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
|
||||
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
|
||||
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
|
||||
import kr.co.vividnext.sodalive.live.room.menu.MenuConfigActivity
|
||||
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
|
||||
import kr.co.vividnext.sodalive.report.CheersReportDialog
|
||||
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import kr.co.vividnext.sodalive.report.UserReportDialog
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -91,6 +98,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
private lateinit var audioContentAdapter: AudioContentAdapter
|
||||
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
|
||||
private lateinit var donationAdapter: UserProfileDonationAdapter
|
||||
private lateinit var channelDonationAdapter: UserProfileChannelDonationAdapter
|
||||
private lateinit var cheersAdapter: UserProfileCheersAdapter
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
@@ -136,6 +144,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
|
||||
setupLiveView()
|
||||
setupDonationView()
|
||||
setupChannelDonationView()
|
||||
setupFanTalkView()
|
||||
setupSeriesListView()
|
||||
setupAudioContentListView()
|
||||
@@ -202,14 +211,15 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
}
|
||||
|
||||
private fun showUserBlockDialog() {
|
||||
val nickname = binding.tvNickname.text
|
||||
val message = if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
getString(R.string.screen_live_room_block_message_creator, nickname)
|
||||
} else {
|
||||
getString(R.string.screen_live_room_block_message_user, nickname)
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setTitle(getString(R.string.screen_live_room_block_title))
|
||||
dialog.setMessage(
|
||||
getString(
|
||||
R.string.screen_live_room_block_message_creator,
|
||||
binding.tvNickname.text
|
||||
)
|
||||
)
|
||||
dialog.setMessage(message)
|
||||
dialog.setPositiveButton(getString(R.string.screen_live_room_block_confirm)) { _, _ ->
|
||||
viewModel.userBlock(userId)
|
||||
}
|
||||
@@ -243,8 +253,8 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
private fun setupLiveView() {
|
||||
val recyclerView = binding.layoutUserProfileLive.rvLive
|
||||
liveAdapter = UserProfileLiveAdapter(
|
||||
onClickParticipant = { enterLiveRoom(roomId = it.roomId) },
|
||||
onClickReservation = { reservationRoom(roomId = it.roomId) }
|
||||
onClickParticipant = { showLiveRoomDetail(roomId = it.roomId) },
|
||||
onClickReservation = { showLiveRoomDetail(roomId = it.roomId) }
|
||||
)
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
@@ -277,6 +287,24 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
recyclerView.adapter = liveAdapter
|
||||
}
|
||||
|
||||
private fun showLiveRoomDetail(roomId: Long) {
|
||||
val detailFragment = LiveRoomDetailFragment(
|
||||
roomId,
|
||||
onClickParticipant = { enterLiveRoom(roomId = roomId) },
|
||||
onClickReservation = { reservationRoom(roomId = roomId) },
|
||||
onClickModify = {},
|
||||
onClickStart = {},
|
||||
onClickCancel = {}
|
||||
)
|
||||
|
||||
if (detailFragment.isAdded) return
|
||||
|
||||
detailFragment.show(
|
||||
supportFragmentManager,
|
||||
detailFragment.tag
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupDonationView() {
|
||||
binding.layoutUserProfileDonation.tvAll.setOnClickListener {
|
||||
val intent = Intent(applicationContext, UserProfileDonationAllViewActivity::class.java)
|
||||
@@ -337,6 +365,73 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
setupCheersView()
|
||||
}
|
||||
|
||||
private fun setupChannelDonationView() {
|
||||
binding.layoutUserProfileChannelDonation.llChannelDonation.visibility =
|
||||
if (userId == SharedPreferenceManager.userId) View.GONE else View.VISIBLE
|
||||
|
||||
binding.layoutUserProfileChannelDonation.tvAll.setOnClickListener {
|
||||
val intent = Intent(applicationContext, UserProfileChannelDonationAllViewActivity::class.java)
|
||||
intent.putExtra(Constants.EXTRA_USER_ID, userId)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.layoutUserProfileChannelDonation.llChannelDonation.setOnClickListener {
|
||||
val dialog = LiveRoomDonationDialog(
|
||||
this,
|
||||
LayoutInflater.from(this),
|
||||
isLiveDonation = true,
|
||||
messageMaxLength = 100,
|
||||
secretToggleLabelResId = R.string.screen_user_profile_channel_donation_secret,
|
||||
applySecretMissionMessageHint = false
|
||||
) { can, message, isSecret ->
|
||||
viewModel.postChannelDonation(
|
||||
creatorId = userId,
|
||||
can = can,
|
||||
isSecret = isSecret,
|
||||
message = message
|
||||
) {
|
||||
viewModel.getCreatorProfile(userId)
|
||||
}
|
||||
}
|
||||
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
|
||||
}
|
||||
|
||||
val recyclerView = binding.layoutUserProfileChannelDonation.rvChannelDonation
|
||||
channelDonationAdapter = UserProfileChannelDonationAdapter()
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
channelDonationAdapter.itemCount - 1 -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
recyclerView.adapter = channelDonationAdapter
|
||||
}
|
||||
|
||||
private fun setupCheersView() {
|
||||
binding.layoutUserProfileFanTalk.ivSend.setOnClickListener {
|
||||
hideKeyboard {
|
||||
@@ -615,9 +710,12 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
it.totalContentCount,
|
||||
it.ownedContentCount
|
||||
)
|
||||
setChannelDonationList(it.channelDonationList)
|
||||
setLiveRoomList(it.liveRoomList)
|
||||
setUserDonationRanking(it.userDonationRanking)
|
||||
setCommunityPostList(it.communityPostList)
|
||||
} else {
|
||||
binding.layoutUserProfileChannelDonation.root.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,6 +809,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
if (creator.creatorId == SharedPreferenceManager.userId) {
|
||||
binding.ivNotification.visibility = View.GONE
|
||||
binding.tvNotificationCount.visibility = View.GONE
|
||||
binding.tvNotificationCount.setOnClickListener(null)
|
||||
binding.tvFollowerList.visibility = View.VISIBLE
|
||||
|
||||
binding.tvFollowerList.setOnClickListener {
|
||||
@@ -725,9 +824,20 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
binding
|
||||
.tvNotificationCount
|
||||
.text = getString(
|
||||
R.string.screen_user_profile_follower_count,
|
||||
creator.notificationRecipientCount.moneyFormat()
|
||||
)
|
||||
R.string.screen_user_profile_follower_count,
|
||||
creator.notificationRecipientCount.moneyFormat()
|
||||
)
|
||||
|
||||
binding.tvNotificationCount.setOnClickListener {
|
||||
viewModel.getCreatorDetail(creator.creatorId) { detail ->
|
||||
CreatorDetailDialog(
|
||||
activity = this@UserProfileActivity,
|
||||
layoutInflater = layoutInflater,
|
||||
screenWidth = screenWidth,
|
||||
detail = detail
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (creator.isFollow) {
|
||||
@@ -902,10 +1012,11 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun setUserDonationRanking(userDonationRanking: List<UserDonationRankingResponse>) {
|
||||
if (userDonationRanking.isEmpty()) {
|
||||
binding.layoutUserProfileDonation.root.visibility = View.GONE
|
||||
} else {
|
||||
binding.layoutUserProfileDonation.root.visibility = View.VISIBLE
|
||||
val isMyProfile = userId == SharedPreferenceManager.userId
|
||||
binding.layoutUserProfileDonation.root.visibility =
|
||||
if (isMyProfile || userDonationRanking.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
|
||||
if (userDonationRanking.isNotEmpty()) {
|
||||
donationAdapter.items.clear()
|
||||
donationAdapter.items.addAll(userDonationRanking)
|
||||
donationAdapter.notifyDataSetChanged()
|
||||
@@ -954,6 +1065,21 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun setChannelDonationList(channelDonationItems: List<GetChannelDonationListItem>) {
|
||||
binding.layoutUserProfileChannelDonation.root.visibility = View.VISIBLE
|
||||
binding.layoutUserProfileChannelDonation.tvNoChannelDonation.visibility =
|
||||
if (channelDonationItems.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.layoutUserProfileChannelDonation.rvChannelDonation.visibility =
|
||||
if (channelDonationItems.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.layoutUserProfileChannelDonation.tvAll.visibility =
|
||||
if (channelDonationItems.isEmpty()) View.GONE else View.VISIBLE
|
||||
|
||||
channelDonationAdapter.items.clear()
|
||||
channelDonationAdapter.items.addAll(channelDonationItems)
|
||||
channelDonationAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun setCommunityPost(
|
||||
layout: ItemCreatorCommunityBinding,
|
||||
item: GetCommunityPostListResponse,
|
||||
|
||||
@@ -12,6 +12,9 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
|
||||
import kr.co.vividnext.sodalive.report.ReportRepository
|
||||
import kr.co.vividnext.sodalive.report.ReportRequest
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
@@ -34,6 +37,10 @@ class UserProfileViewModel(
|
||||
val creatorProfileLiveData: LiveData<GetCreatorProfileResponse>
|
||||
get() = _creatorProfileLiveData
|
||||
|
||||
private val _channelDonationLiveData = MutableLiveData<GetChannelDonationListResponse>()
|
||||
val channelDonationLiveData: LiveData<GetChannelDonationListResponse>
|
||||
get() = _channelDonationLiveData
|
||||
|
||||
fun cheersReport(cheersId: Long, reason: String) {
|
||||
_isLoading.value = true
|
||||
|
||||
@@ -156,6 +163,43 @@ class UserProfileViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun getCreatorDetail(userId: Long, onSuccess: (GetCreatorDetailResponse) -> Unit) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getCreatorDetail(
|
||||
id = userId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success && it.data != null) {
|
||||
onSuccess(it.data)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
@@ -318,6 +362,89 @@ class UserProfileViewModel(
|
||||
onSuccess("보이스온 ${nickname}님의 채널입니다.\n$shareUrl")
|
||||
}
|
||||
|
||||
fun getChannelDonationList(creatorId: Long) {
|
||||
compositeDisposable.add(
|
||||
repository.getChannelDonationList(
|
||||
creatorId = creatorId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_channelDonationLiveData.postValue(it.data)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun postChannelDonation(
|
||||
creatorId: Long,
|
||||
can: Int,
|
||||
isSecret: Boolean,
|
||||
message: String,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.postChannelDonation(
|
||||
request = PostChannelDonationRequest(
|
||||
creatorId = creatorId,
|
||||
can = can,
|
||||
isSecret = isSecret,
|
||||
message = message
|
||||
),
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success) {
|
||||
SharedPreferenceManager.can -= can
|
||||
onSuccess()
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun userBlock(userId: Long) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import android.content.Context
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun GetChannelDonationListItem.relativeTimeText(context: Context): String {
|
||||
val pastMillis = parseServerUtcToMillis(createdAt)
|
||||
?: return context.getString(R.string.character_comment_time_just_now)
|
||||
|
||||
val nowMillis = System.currentTimeMillis()
|
||||
var diff = nowMillis - pastMillis
|
||||
if (diff < 0) diff = 0
|
||||
|
||||
val minute = 60_000L
|
||||
val hour = 60 * minute
|
||||
val day = 24 * hour
|
||||
|
||||
if (diff < minute) {
|
||||
return context.getString(R.string.character_comment_time_just_now)
|
||||
}
|
||||
|
||||
if (diff < hour) {
|
||||
val minutes = (diff / minute).toInt()
|
||||
return context.getString(R.string.character_comment_time_minutes, minutes)
|
||||
}
|
||||
|
||||
if (diff < day) {
|
||||
val hours = (diff / hour).toInt()
|
||||
return context.getString(R.string.character_comment_time_hours, hours)
|
||||
}
|
||||
|
||||
if (diff < 30 * day) {
|
||||
val days = (diff / day).toInt()
|
||||
return context.getString(R.string.character_comment_time_days, days)
|
||||
}
|
||||
|
||||
val tz = TimeZone.getDefault()
|
||||
val calNow = Calendar.getInstance(tz, Locale.getDefault())
|
||||
val calPast = Calendar.getInstance(tz, Locale.getDefault())
|
||||
calPast.timeInMillis = pastMillis
|
||||
|
||||
var years = calNow.get(Calendar.YEAR) - calPast.get(Calendar.YEAR)
|
||||
val nowMonth = calNow.get(Calendar.MONTH)
|
||||
val pastMonth = calPast.get(Calendar.MONTH)
|
||||
val nowDay = calNow.get(Calendar.DAY_OF_MONTH)
|
||||
val pastDay = calPast.get(Calendar.DAY_OF_MONTH)
|
||||
if (nowMonth < pastMonth || (nowMonth == pastMonth && nowDay < pastDay)) {
|
||||
years -= 1
|
||||
}
|
||||
|
||||
if (years < 1) {
|
||||
var months = (calNow.get(Calendar.YEAR) - calPast.get(Calendar.YEAR)) * 12 + (nowMonth - pastMonth)
|
||||
if (nowDay < pastDay) months -= 1
|
||||
if (months < 1) months = 1
|
||||
return context.getString(R.string.character_comment_time_months, months)
|
||||
}
|
||||
|
||||
return context.getString(R.string.character_comment_time_years, years)
|
||||
}
|
||||
|
||||
private fun parseServerUtcToMillis(dateUtc: String?): Long? {
|
||||
if (dateUtc.isNullOrBlank()) return null
|
||||
val value = dateUtc.trim()
|
||||
|
||||
if (value.all { it.isDigit() }) {
|
||||
return try { value.toLong() } catch (_: NumberFormatException) { null }
|
||||
}
|
||||
|
||||
val patterns = listOf(
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy/MM/dd HH:mm:ss"
|
||||
)
|
||||
|
||||
for (pattern in patterns) {
|
||||
try {
|
||||
val sdf = SimpleDateFormat(pattern, Locale.US)
|
||||
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsed: Date? = sdf.parse(value)
|
||||
if (parsed != null) return parsed.time
|
||||
} catch (_: ParseException) { }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
@DrawableRes
|
||||
fun GetChannelDonationListItem.chatDonationBackgroundRes(): Int {
|
||||
if (isSecret) return R.drawable.bg_round_corner_6_7_cc59548f
|
||||
return when {
|
||||
can >= 10000 -> R.drawable.bg_round_corner_6_7_ccc25264
|
||||
can >= 5000 -> R.drawable.bg_round_corner_6_7_ccd85e37
|
||||
can >= 1000 -> R.drawable.bg_round_corner_6_7_ccd38c38
|
||||
can >= 500 -> R.drawable.bg_round_corner_6_7_cc59548f
|
||||
can >= 100 -> R.drawable.bg_round_corner_6_7_cc4d6aa4
|
||||
can >= 50 -> R.drawable.bg_round_corner_6_7_cc2d7390
|
||||
else -> R.drawable.bg_round_corner_6_7_cc548f7d
|
||||
}
|
||||
}
|
||||
|
||||
fun GetChannelDonationListItem.channelDonationContentText(
|
||||
context: Context,
|
||||
maxLength: Int? = null
|
||||
) =
|
||||
run {
|
||||
val contentText = if (maxLength != null && message.length > maxLength) {
|
||||
"${message.take(maxLength)}..."
|
||||
} else {
|
||||
message
|
||||
}
|
||||
val canRange = Regex("[0-9,]+\\s*캔").find(contentText)?.range
|
||||
SpannableString(contentText).apply {
|
||||
setSpan(
|
||||
ForegroundColorSpan(
|
||||
ContextCompat.getColor(context, R.color.color_cfd8dc)
|
||||
),
|
||||
0,
|
||||
contentText.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
if (canRange != null) {
|
||||
setSpan(
|
||||
ForegroundColorSpan(
|
||||
ContextCompat.getColor(context, R.color.color_fdca2f)
|
||||
),
|
||||
canRange.first,
|
||||
canRange.last + 1,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetChannelDonationListResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetChannelDonationListItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetChannelDonationListItem(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("memberId") val memberId: Long,
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileUrl") val profileUrl: String,
|
||||
@SerializedName("can") val can: Int,
|
||||
@SerializedName("isSecret") val isSecret: Boolean,
|
||||
@SerializedName("message") val message: String,
|
||||
@SerializedName("createdAt") val createdAt: String
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class PostChannelDonationRequest(
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("can") val can: Int,
|
||||
@SerializedName("isSecret") val isSecret: Boolean = false,
|
||||
@SerializedName("message") val message: String = "",
|
||||
@SerializedName("container") val container: String = "aos"
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemUserProfileChannelDonationBinding
|
||||
|
||||
class UserProfileChannelDonationAdapter : RecyclerView.Adapter<UserProfileChannelDonationAdapter.ViewHolder>() {
|
||||
|
||||
val items = mutableListOf<GetChannelDonationListItem>()
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: ItemUserProfileChannelDonationBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetChannelDonationListItem) {
|
||||
binding.ivProfile.load(item.profileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.tvNickname.text = item.nickname
|
||||
binding.tvDate.text = item.relativeTimeText(binding.root.context)
|
||||
binding.tvContent.text = item.channelDonationContentText(
|
||||
context = binding.root.context,
|
||||
maxLength = 30
|
||||
)
|
||||
binding.root.setBackgroundResource(item.chatDonationBackgroundRes())
|
||||
binding.root.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemUserProfileChannelDonationBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemUserProfileChannelDonationAllBinding
|
||||
|
||||
class UserProfileChannelDonationAllAdapter : RecyclerView.Adapter<UserProfileChannelDonationAllAdapter.ViewHolder>() {
|
||||
|
||||
val items = mutableListOf<GetChannelDonationListItem>()
|
||||
private val expandedItemIds = mutableSetOf<Long>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemUserProfileChannelDonationAllBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetChannelDonationListItem) {
|
||||
binding.ivProfile.load(item.profileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.tvNickname.text = item.nickname
|
||||
binding.tvDate.text = item.relativeTimeText(binding.root.context)
|
||||
val isExpanded = expandedItemIds.contains(item.id)
|
||||
binding.tvContent.text = item.channelDonationContentText(
|
||||
context = binding.root.context,
|
||||
maxLength = if (isExpanded) null else 30
|
||||
)
|
||||
binding.root.setBackgroundResource(item.chatDonationBackgroundRes())
|
||||
binding.tvContent.setOnClickListener {
|
||||
if (!isExpanded && item.message.length > 30) {
|
||||
expandedItemIds.add(item.id)
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemUserProfileChannelDonationAllBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileChannelDonationAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class UserProfileChannelDonationAllViewActivity : BaseActivity<ActivityUserProfileChannelDonationAllBinding>(
|
||||
ActivityUserProfileChannelDonationAllBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: UserProfileChannelDonationAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: UserProfileChannelDonationAllAdapter
|
||||
|
||||
private var userId: Long = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (userId > 0) {
|
||||
bindData()
|
||||
viewModel.getChannelDonationList(userId)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.error_invalid_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.setText(R.string.screen_user_profile_channel_donation_all_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
val recyclerView = binding.rvChannelDonation
|
||||
adapter = UserProfileChannelDonationAllAdapter()
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
outRect.bottom = 10.dpToPx().toInt()
|
||||
}
|
||||
})
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.channelDonationLiveData.observe(this) {
|
||||
binding.tvTotalCount.text = it.totalCount.toString()
|
||||
adapter.items.clear()
|
||||
adapter.items.addAll(it.items)
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.tvNoChannelDonation.visibility = if (it.items.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.rvChannelDonation.visibility = if (it.items.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
|
||||
|
||||
class UserProfileChannelDonationAllViewModel(
|
||||
private val repository: ExplorerRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private val _channelDonationLiveData = MutableLiveData<GetChannelDonationListResponse>()
|
||||
val channelDonationLiveData: LiveData<GetChannelDonationListResponse>
|
||||
get() = _channelDonationLiveData
|
||||
|
||||
fun getChannelDonationList(creatorId: Long) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getChannelDonationList(
|
||||
creatorId = creatorId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success && it.data != null) {
|
||||
_channelDonationLiveData.postValue(it.data)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,12 @@ class UserProfileCheersAdapter(
|
||||
binding.tvModify.setOnClickListener {
|
||||
binding.rlContentModify.visibility = View.GONE
|
||||
binding.tvContent.visibility = View.VISIBLE
|
||||
modifyCheers(cheers.cheersId, binding.etContentModify.text.toString())
|
||||
|
||||
val content = binding.etContentModify.text.toString()
|
||||
binding.etContentModify.setText("")
|
||||
if (content.isEmpty()) return@setOnClickListener
|
||||
|
||||
modifyCheers(cheers.cheersId, content)
|
||||
}
|
||||
} else {
|
||||
binding.ivMenu.visibility = View.GONE
|
||||
@@ -100,6 +105,8 @@ class UserProfileCheersAdapter(
|
||||
binding.rlCheerReply.visibility = View.VISIBLE
|
||||
binding.tvSend.setOnClickListener {
|
||||
val content = binding.etCheerReply.text.toString()
|
||||
binding.etCheerReply.setText("")
|
||||
if (content.isEmpty()) return@setOnClickListener
|
||||
modifyReply(reply.cheersId, content)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +121,8 @@ class UserProfileCheersAdapter(
|
||||
binding.rlCheerReply.visibility = View.VISIBLE
|
||||
binding.tvSend.setOnClickListener {
|
||||
val content = binding.etCheerReply.text.toString()
|
||||
binding.etCheerReply.setText("")
|
||||
if (content.isEmpty()) return@setOnClickListener
|
||||
enterReply(cheers.cheersId, content)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +131,8 @@ class UserProfileCheersAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
binding.etCheerReply.setText("")
|
||||
binding.etContentModify.setText("")
|
||||
binding.tvReply.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,19 @@ import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityAllBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreatorCommunityCommentFragment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||
@@ -28,15 +32,67 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
ActivityCreatorCommunityAllBinding::inflate
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val GRID_SPAN_COUNT = 3
|
||||
}
|
||||
|
||||
private val viewModel: CreatorCommunityAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: CreatorCommunityAllAdapter
|
||||
private lateinit var listAdapter: CreatorCommunityAllAdapter
|
||||
private lateinit var gridAdapter: CreatorCommunityAllGridAdapter
|
||||
private lateinit var mediaPlayerManager: CreatorCommunityMediaPlayerManager
|
||||
private lateinit var imm: InputMethodManager
|
||||
|
||||
private var creatorId: Long = 0
|
||||
private var deepLinkTargetPostId: Long = 0
|
||||
private var isDeepLinkCommentHandled = false
|
||||
private var isListMode = false
|
||||
private var listAnchorPosition = 0
|
||||
private var gridAnchorPosition = 0
|
||||
private var isListEnteredFromGridClick = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val gridSpacingPx by lazy { 1.3f.dpToPx().toInt() }
|
||||
private val contentEdgePaddingPx by lazy { 13.3f.dpToPx().toInt() }
|
||||
private val gridItemDecoration by lazy {
|
||||
GridSpacingItemDecoration(
|
||||
spanCount = GRID_SPAN_COUNT,
|
||||
spacing = gridSpacingPx,
|
||||
includeEdge = true
|
||||
)
|
||||
}
|
||||
private val listItemDecoration = object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
RecyclerView.NO_POSITION -> Unit
|
||||
|
||||
0 -> {
|
||||
outRect.top = 13.3f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
listAdapter.itemCount - 1 -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val modifyResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
@@ -54,10 +110,16 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) {
|
||||
adapter.updateUI()
|
||||
if (::listAdapter.isInitialized) {
|
||||
listAdapter.updateUI()
|
||||
}
|
||||
}
|
||||
|
||||
creatorId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0)
|
||||
deepLinkTargetPostId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0)
|
||||
if (deepLinkTargetPostId <= 0) {
|
||||
deepLinkTargetPostId = intent.getStringExtra(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull() ?: 0
|
||||
}
|
||||
if (creatorId <= 0) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
@@ -65,6 +127,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
bindData()
|
||||
@@ -87,9 +150,29 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
binding.toolbar.tvBack.setText(R.string.screen_creator_community_all_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
binding.toolbar.tvBack.setOnClickListener {
|
||||
handleBackNavigation()
|
||||
}
|
||||
binding.llTabList.setOnClickListener {
|
||||
if (isListMode) {
|
||||
isListEnteredFromGridClick = false
|
||||
updateTabUi()
|
||||
} else {
|
||||
switchToListMode(gridAnchorPosition, fromGridItemClick = false)
|
||||
}
|
||||
}
|
||||
binding.llTabGrid.setOnClickListener {
|
||||
if (isListMode) {
|
||||
switchToGridMode(anchorPosition = listAnchorPosition)
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
handleBackNavigation()
|
||||
}
|
||||
})
|
||||
|
||||
adapter = CreatorCommunityAllAdapter(
|
||||
listAdapter = CreatorCommunityAllAdapter(
|
||||
screenWidth = screenWidth,
|
||||
onClickLike = { viewModel.communityPostLike(it) },
|
||||
writeComment = { postId, parentId, comment, isSecret ->
|
||||
@@ -102,15 +185,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
)
|
||||
},
|
||||
showCommentBottomSheetDialog = { postId, existOrdered ->
|
||||
val dialog = CreatorCommunityCommentFragment(
|
||||
creatorId = creatorId,
|
||||
postId = postId,
|
||||
existOrdered = existOrdered
|
||||
)
|
||||
dialog.show(
|
||||
supportFragmentManager,
|
||||
dialog.tag
|
||||
)
|
||||
showCommentBottomSheet(postId = postId, existOrdered = existOrdered)
|
||||
},
|
||||
onClickModify = {
|
||||
modifyResult.launch(
|
||||
@@ -154,65 +229,25 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
confirmButtonClick = {
|
||||
viewModel.purchaseCommunityPost(postId) {
|
||||
onSuccess(it)
|
||||
updateGridItem(it)
|
||||
}
|
||||
}
|
||||
).show(screenWidth)
|
||||
}
|
||||
)
|
||||
|
||||
val recyclerView = binding.rvCreatorCommunity
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
val gridItemSize = ((screenWidth - (gridSpacingPx * (GRID_SPAN_COUNT + 1))) / GRID_SPAN_COUNT)
|
||||
.coerceAtLeast(1)
|
||||
gridAdapter = CreatorCommunityAllGridAdapter(
|
||||
itemSize = gridItemSize,
|
||||
onClickItem = {
|
||||
switchToListMode(it, fromGridItemClick = true)
|
||||
}
|
||||
)
|
||||
|
||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
setupRecyclerViews()
|
||||
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 13.3f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisiblePosition = (recyclerView.layoutManager as LinearLayoutManager)
|
||||
.findLastVisibleItemPosition()
|
||||
val itemTotalCount = adapter.itemCount - 1
|
||||
|
||||
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
|
||||
viewModel.getCommunityPostList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
switchToListMode(0, fromGridItemClick = false)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@@ -226,16 +261,195 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
openDeepLinkTargetCommentIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.communityPostListLiveData.observe(this) {
|
||||
if (viewModel.page == 2) {
|
||||
adapter.items.clear()
|
||||
listAdapter.items.clear()
|
||||
gridAdapter.items.clear()
|
||||
}
|
||||
|
||||
adapter.items.addAll(it)
|
||||
adapter.notifyDataSetChanged()
|
||||
listAdapter.items.addAll(it)
|
||||
gridAdapter.items.addAll(it)
|
||||
listAdapter.notifyDataSetChanged()
|
||||
gridAdapter.notifyDataSetChanged()
|
||||
openDeepLinkTargetCommentIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDeepLinkTargetCommentIfNeeded() {
|
||||
if (isDeepLinkCommentHandled || deepLinkTargetPostId <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val targetPost = listAdapter.items.firstOrNull { it.postId == deepLinkTargetPostId }
|
||||
if (targetPost != null) {
|
||||
isDeepLinkCommentHandled = true
|
||||
switchToListMode(listAdapter.items.indexOfFirst { it.postId == targetPost.postId }, fromGridItemClick = false)
|
||||
showCommentBottomSheet(postId = targetPost.postId, existOrdered = targetPost.existOrdered)
|
||||
return
|
||||
}
|
||||
|
||||
if (viewModel.isLast) {
|
||||
isDeepLinkCommentHandled = true
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.screen_creator_community_all_error_invalid_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (viewModel.isLoading.value != true) {
|
||||
viewModel.getCommunityPostList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCommentBottomSheet(postId: Long, existOrdered: Boolean) {
|
||||
val dialog = CreatorCommunityCommentFragment(
|
||||
creatorId = creatorId,
|
||||
postId = postId,
|
||||
existOrdered = existOrdered
|
||||
)
|
||||
dialog.show(
|
||||
supportFragmentManager,
|
||||
dialog.tag
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupRecyclerViews() {
|
||||
val listRecyclerView = binding.rvCreatorCommunity
|
||||
listRecyclerView.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
listRecyclerView.adapter = listAdapter
|
||||
listRecyclerView.setHasFixedSize(true)
|
||||
listRecyclerView.itemAnimator = null
|
||||
if (listRecyclerView.itemDecorationCount == 0) {
|
||||
listRecyclerView.addItemDecoration(listItemDecoration)
|
||||
}
|
||||
listRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
||||
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
|
||||
listAnchorPosition = firstVisiblePosition
|
||||
}
|
||||
|
||||
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
|
||||
val itemTotalCount = listAdapter.itemCount - 1
|
||||
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
|
||||
viewModel.getCommunityPostList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val gridRecyclerView = binding.rvCreatorCommunityGrid
|
||||
gridRecyclerView.layoutManager = GridLayoutManager(applicationContext, GRID_SPAN_COUNT)
|
||||
gridRecyclerView.adapter = gridAdapter
|
||||
gridRecyclerView.setHasFixedSize(true)
|
||||
gridRecyclerView.itemAnimator = null
|
||||
gridRecyclerView.setPadding(0, contentEdgePaddingPx, 0, contentEdgePaddingPx)
|
||||
gridRecyclerView.clipToPadding = false
|
||||
if (gridRecyclerView.itemDecorationCount == 0) {
|
||||
gridRecyclerView.addItemDecoration(gridItemDecoration)
|
||||
}
|
||||
gridRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val layoutManager = recyclerView.layoutManager as? GridLayoutManager ?: return
|
||||
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
|
||||
gridAnchorPosition = firstVisiblePosition
|
||||
}
|
||||
|
||||
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
|
||||
val itemTotalCount = gridAdapter.itemCount - 1
|
||||
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
|
||||
viewModel.getCommunityPostList()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun handleBackNavigation() {
|
||||
if (isListMode && isListEnteredFromGridClick) {
|
||||
switchToGridMode(anchorPosition = listAnchorPosition)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToListMode(position: Int, fromGridItemClick: Boolean) {
|
||||
val requestedPosition = position.coerceAtLeast(0)
|
||||
isListMode = true
|
||||
isListEnteredFromGridClick = fromGridItemClick
|
||||
listAnchorPosition = requestedPosition
|
||||
updateTabUi()
|
||||
|
||||
binding.rvCreatorCommunity.visibility = View.VISIBLE
|
||||
binding.rvCreatorCommunityGrid.visibility = View.INVISIBLE
|
||||
|
||||
val recyclerView = binding.rvCreatorCommunity
|
||||
|
||||
recyclerView.post {
|
||||
if (listAdapter.itemCount > 0) {
|
||||
val targetPosition = requestedPosition.coerceIn(0, listAdapter.itemCount - 1)
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(targetPosition, 0)
|
||||
listAnchorPosition = targetPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToGridMode(anchorPosition: Int = 0) {
|
||||
isListMode = false
|
||||
isListEnteredFromGridClick = false
|
||||
updateTabUi()
|
||||
if (::mediaPlayerManager.isInitialized) {
|
||||
mediaPlayerManager.pauseContent()
|
||||
}
|
||||
|
||||
binding.rvCreatorCommunity.visibility = View.INVISIBLE
|
||||
binding.rvCreatorCommunityGrid.visibility = View.VISIBLE
|
||||
|
||||
val recyclerView = binding.rvCreatorCommunityGrid
|
||||
|
||||
recyclerView.post {
|
||||
if (gridAdapter.itemCount > 0) {
|
||||
val targetPosition = anchorPosition.coerceIn(0, gridAdapter.itemCount - 1)
|
||||
recyclerView.scrollToPosition(targetPosition)
|
||||
gridAnchorPosition = targetPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabUi() {
|
||||
if (isListMode) {
|
||||
binding.ivTabList.setImageResource(R.drawable.ic_community_list_selected)
|
||||
binding.ivTabGrid.setImageResource(R.drawable.ic_community_grid)
|
||||
binding.vTabListIndicator.visibility = View.VISIBLE
|
||||
binding.vTabGridIndicator.visibility = View.INVISIBLE
|
||||
} else {
|
||||
binding.ivTabList.setImageResource(R.drawable.ic_community_list)
|
||||
binding.ivTabGrid.setImageResource(R.drawable.ic_community_grid_selected)
|
||||
binding.vTabListIndicator.visibility = View.INVISIBLE
|
||||
binding.vTabGridIndicator.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGridItem(updatedPost: GetCommunityPostListResponse) {
|
||||
val index = gridAdapter.items.indexOfFirst { it.postId == updatedPost.postId }
|
||||
if (index >= 0) {
|
||||
gridAdapter.items[index] = updatedPost
|
||||
gridAdapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.LayoutInflater
|
||||
@@ -46,6 +47,10 @@ class CreatorCommunityAllAdapter(
|
||||
(Long, Int, onSuccess: (GetCommunityPostListResponse) -> Unit) -> Unit
|
||||
) : RecyclerView.Adapter<CreatorCommunityAllAdapter.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_PREVIEW_MAX_LENGTH = 120
|
||||
}
|
||||
|
||||
val items = mutableListOf<GetCommunityPostListResponse>()
|
||||
|
||||
inner class ViewHolder(
|
||||
@@ -257,26 +262,42 @@ class CreatorCommunityAllAdapter(
|
||||
index: Int
|
||||
) {
|
||||
textView.visibility = View.VISIBLE
|
||||
textView.text = text
|
||||
|
||||
val spannable = SpannableString(text)
|
||||
val pattern = Pattern.compile("https?://\\S+")
|
||||
val matcher = pattern.matcher(spannable)
|
||||
if (isExpand) {
|
||||
val spannable = SpannableString(text)
|
||||
val pattern = Pattern.compile("https?://\\S+")
|
||||
val matcher = pattern.matcher(spannable)
|
||||
|
||||
while (matcher.find()) {
|
||||
val start = matcher.start()
|
||||
val end = matcher.end()
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val url = spannable.subSequence(start, end).toString()
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
while (matcher.find()) {
|
||||
val start = matcher.start()
|
||||
val end = matcher.end()
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val url = spannable.subSequence(start, end).toString()
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
textView.text = spannable
|
||||
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||
textView.maxLines = Int.MAX_VALUE
|
||||
textView.ellipsize = null
|
||||
textView.linksClickable = true
|
||||
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||
textView.text = spannable
|
||||
} else {
|
||||
val collapsedText = if (text.length > CONTENT_PREVIEW_MAX_LENGTH) {
|
||||
"${text.take(CONTENT_PREVIEW_MAX_LENGTH)}..."
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
textView.maxLines = 3
|
||||
textView.ellipsize = TextUtils.TruncateAt.END
|
||||
textView.linksClickable = false
|
||||
textView.movementMethod = null
|
||||
textView.text = collapsedText
|
||||
}
|
||||
|
||||
textView.setOnClickListener {
|
||||
items[index] = items[index].copy(
|
||||
@@ -284,12 +305,6 @@ class CreatorCommunityAllAdapter(
|
||||
)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
textView.maxLines = if (isExpand) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityAllGridBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
|
||||
class CreatorCommunityAllGridAdapter(
|
||||
private val itemSize: Int,
|
||||
private val onClickItem: (Int) -> Unit
|
||||
) : RecyclerView.Adapter<CreatorCommunityAllGridAdapter.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_PREVIEW_MAX_LENGTH = 24
|
||||
}
|
||||
|
||||
val items = mutableListOf<GetCommunityPostListResponse>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemCreatorCommunityAllGridBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: GetCommunityPostListResponse) {
|
||||
val lp = binding.root.layoutParams
|
||||
lp.width = itemSize
|
||||
lp.height = itemSize
|
||||
binding.root.layoutParams = lp
|
||||
|
||||
val isPaidLocked = item.price > 0 && !item.existOrdered
|
||||
val hasImage = !item.imageUrl.isNullOrBlank()
|
||||
|
||||
when {
|
||||
isPaidLocked -> {
|
||||
binding.ivGridImage.visibility = View.GONE
|
||||
binding.tvGridText.visibility = View.GONE
|
||||
binding.ivGridLock.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
hasImage -> {
|
||||
binding.ivGridImage.visibility = View.VISIBLE
|
||||
binding.tvGridText.visibility = View.GONE
|
||||
binding.ivGridLock.visibility = View.GONE
|
||||
binding.ivGridImage.loadUrl(item.imageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.ivGridImage.visibility = View.GONE
|
||||
binding.tvGridText.visibility = View.VISIBLE
|
||||
binding.ivGridLock.visibility = View.GONE
|
||||
binding.tvGridText.text = item.content
|
||||
.replace("\n", " ")
|
||||
.trim()
|
||||
.take(CONTENT_PREVIEW_MAX_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onClickItem(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemCreatorCommunityAllGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.detail
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.net.toUri
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogCreatorDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
class CreatorDetailDialog(
|
||||
private val activity: Activity,
|
||||
layoutInflater: LayoutInflater,
|
||||
private val screenWidth: Int,
|
||||
private val detail: GetCreatorDetailResponse
|
||||
) {
|
||||
|
||||
private data class SnsItem(
|
||||
val url: String,
|
||||
val iconResId: Int
|
||||
)
|
||||
|
||||
private val alertDialog: AlertDialog
|
||||
private val dialogView = DialogCreatorDetailBinding.inflate(layoutInflater)
|
||||
|
||||
init {
|
||||
val dialogBuilder = AlertDialog.Builder(activity)
|
||||
dialogBuilder.setView(dialogView.root)
|
||||
|
||||
alertDialog = dialogBuilder.create()
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
dialogView.ivClose.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
dialogView.ivProfile.load(detail.profileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(16f.dpToPx()))
|
||||
}
|
||||
dialogView.tvNickname.text = detail.nickname
|
||||
|
||||
dialogView.tvDebutValue.text = getDebutValue()
|
||||
dialogView.tvLiveCountValue.text = detail.activitySummary.liveCount.moneyFormat()
|
||||
dialogView.tvLiveTimeValue.text = detail.activitySummary.liveTime.moneyFormat()
|
||||
dialogView.tvLiveContributorCountValue.text = detail.activitySummary.liveContributorCount.moneyFormat()
|
||||
dialogView.tvContentCountValue.text = detail.activitySummary.contentCount.moneyFormat()
|
||||
|
||||
bindSnsItems()
|
||||
}
|
||||
|
||||
private fun getDebutValue(): String {
|
||||
val debutDate = detail.debutDate.trim()
|
||||
val dDay = detail.dDay.trim()
|
||||
if (debutDate.isBlank() && dDay.isBlank()) {
|
||||
return activity.getString(R.string.screen_creator_detail_debut_before)
|
||||
}
|
||||
|
||||
return "$debutDate ($dDay)"
|
||||
}
|
||||
|
||||
private fun bindSnsItems() {
|
||||
val snsItems = listOf(
|
||||
SnsItem(
|
||||
url = detail.youtubeUrl.trim(),
|
||||
iconResId = R.drawable.ic_sns_youtube
|
||||
),
|
||||
SnsItem(
|
||||
url = detail.instagramUrl.trim(),
|
||||
iconResId = R.drawable.ic_sns_instagram
|
||||
),
|
||||
SnsItem(
|
||||
url = detail.kakaoOpenChatUrl.trim(),
|
||||
iconResId = R.drawable.ic_sns_kakao
|
||||
),
|
||||
SnsItem(
|
||||
url = detail.fancimmUrl.trim(),
|
||||
iconResId = R.drawable.ic_sns_fancimm
|
||||
),
|
||||
SnsItem(
|
||||
url = detail.xUrl.trim(),
|
||||
iconResId = R.drawable.ic_sns_x
|
||||
)
|
||||
).filter { item ->
|
||||
item.url.isNotBlank() && URLUtil.isValidUrl(item.url)
|
||||
}
|
||||
|
||||
if (snsItems.isEmpty()) {
|
||||
dialogView.llSectionSns.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
dialogView.llSectionSns.visibility = View.VISIBLE
|
||||
dialogView.llSnsIcons.removeAllViews()
|
||||
|
||||
snsItems.forEachIndexed { index, item ->
|
||||
val imageView = ImageView(activity).apply {
|
||||
setImageResource(item.iconResId)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
if (index > 0) {
|
||||
marginStart = 16.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
setOnClickListener {
|
||||
openUrl(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
dialogView.llSnsIcons.addView(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
if (intent.resolveActivity(activity.packageManager) != null) {
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
|
||||
fun show() {
|
||||
alertDialog.show()
|
||||
|
||||
val lp = WindowManager.LayoutParams()
|
||||
lp.copyFrom(alertDialog.window?.attributes)
|
||||
lp.width = screenWidth - (48.dpToPx()).toInt()
|
||||
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
|
||||
alertDialog.window?.attributes = lp
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.detail
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorActivitySummary
|
||||
|
||||
@Keep
|
||||
data class GetCreatorDetailResponse(
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String,
|
||||
@SerializedName("debutDate") val debutDate: String,
|
||||
@SerializedName("dday") val dDay: String,
|
||||
@SerializedName("activitySummary") val activitySummary: GetCreatorActivitySummary,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String,
|
||||
@SerializedName("xurl") val xUrl: String,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.donation
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
enum class DonationRankingPeriod {
|
||||
@SerializedName("WEEKLY")
|
||||
WEEKLY,
|
||||
@SerializedName("CUMULATIVE")
|
||||
CUMULATIVE
|
||||
}
|
||||
@@ -16,6 +16,8 @@ data class GetDonationAllResponse(
|
||||
val isVisibleDonationRank: Boolean,
|
||||
@SerializedName("totalCount")
|
||||
val totalCount: Int,
|
||||
@SerializedName("donationRankingPeriod")
|
||||
val donationRankingPeriod: DonationRankingPeriod? = null,
|
||||
@SerializedName("userDonationRanking")
|
||||
val userDonationRanking: List<UserDonationRankingResponse>,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
@@ -27,15 +28,19 @@ class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveA
|
||||
private lateinit var adapter: UserProfileDonationAllAdapter
|
||||
|
||||
private var userId: Long = 0
|
||||
private var isCreator: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
|
||||
isCreator = SharedPreferenceManager.userId == userId
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (userId > 0) {
|
||||
bindData()
|
||||
|
||||
viewModel.getCreatorProfileDonationRanking(userId)
|
||||
if (!isCreator) {
|
||||
viewModel.getCreatorProfileDonationRanking(userId)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
@@ -120,15 +125,26 @@ class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveA
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
if (SharedPreferenceManager.userId == userId) {
|
||||
if (isCreator) {
|
||||
binding.llTotal.visibility = View.VISIBLE
|
||||
binding.llVisibleDonationRanking.visibility = View.VISIBLE
|
||||
binding.llDonationRankingPeriod.visibility = View.VISIBLE
|
||||
binding.tabDonationRankingPeriod.visibility = View.VISIBLE
|
||||
binding.ivVisibleDonationRank.setOnClickListener {
|
||||
viewModel.onClickToggleVisibleDonationRank()
|
||||
}
|
||||
binding.llPeriodWeekly.setOnClickListener {
|
||||
viewModel.updateDonationRankingPeriod(DonationRankingPeriod.WEEKLY)
|
||||
}
|
||||
binding.llPeriodCumulative.setOnClickListener {
|
||||
viewModel.updateDonationRankingPeriod(DonationRankingPeriod.CUMULATIVE)
|
||||
}
|
||||
setupDonationRankingTabs()
|
||||
} else {
|
||||
binding.llTotal.visibility = View.GONE
|
||||
binding.llVisibleDonationRanking.visibility = View.GONE
|
||||
binding.llDonationRankingPeriod.visibility = View.GONE
|
||||
binding.tabDonationRankingPeriod.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +168,15 @@ class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveA
|
||||
binding.tvCanThisMonth.text = it.accumulatedCansThisMonth.moneyFormat()
|
||||
|
||||
binding.tvTotalCount.text = "${it.totalCount}"
|
||||
adapter.items.addAll(it.userDonationRanking)
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
if (it.totalCount > 0) {
|
||||
adapter.items.addAll(it.userDonationRanking)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.selectedDonationRankingPeriod.observe(this) {
|
||||
updateDonationRankingPeriodSelection(it)
|
||||
}
|
||||
|
||||
viewModel.isVisibleDonationRank.observe(this) {
|
||||
@@ -166,4 +189,57 @@ class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveA
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDonationRankingTabs() {
|
||||
binding.tabDonationRankingPeriod.apply {
|
||||
addTab(
|
||||
newTab()
|
||||
.setText(R.string.screen_user_profile_donation_period_weekly)
|
||||
.setTag(DonationRankingPeriod.WEEKLY)
|
||||
)
|
||||
addTab(
|
||||
newTab()
|
||||
.setText(R.string.screen_user_profile_donation_period_cumulative)
|
||||
.setTag(DonationRankingPeriod.CUMULATIVE)
|
||||
)
|
||||
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
val period = tab.tag as DonationRankingPeriod
|
||||
adapter.items.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
viewModel.refresh(userId, period)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
val period = tab.tag as DonationRankingPeriod
|
||||
adapter.items.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
viewModel.refresh(userId, period)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
binding.tabDonationRankingPeriod.getTabAt(0)?.select()
|
||||
}
|
||||
|
||||
private fun updateDonationRankingPeriodSelection(period: DonationRankingPeriod) {
|
||||
binding.ivPeriodWeekly.setImageResource(
|
||||
if (period == DonationRankingPeriod.WEEKLY) {
|
||||
R.drawable.btn_square_select_checked
|
||||
} else {
|
||||
R.drawable.btn_square_select_normal
|
||||
}
|
||||
)
|
||||
binding.ivPeriodCumulative.setImageResource(
|
||||
if (period == DonationRankingPeriod.CUMULATIVE) {
|
||||
R.drawable.btn_square_select_checked
|
||||
} else {
|
||||
R.drawable.btn_square_select_normal
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +34,29 @@ class UserProfileDonationAllViewModel(
|
||||
val isVisibleDonationRank: LiveData<Boolean>
|
||||
get() = _isVisibleDonationRank
|
||||
|
||||
private val _selectedDonationRankingPeriod = MutableLiveData<DonationRankingPeriod>()
|
||||
val selectedDonationRankingPeriod: LiveData<DonationRankingPeriod>
|
||||
get() = _selectedDonationRankingPeriod
|
||||
|
||||
private var isLast = false
|
||||
private var page = 1
|
||||
private val size = 10
|
||||
private var currentRankingPeriod: DonationRankingPeriod? = null
|
||||
|
||||
fun getCreatorProfileDonationRanking(userId: Long) {
|
||||
fun getCreatorProfileDonationRanking(userId: Long, period: DonationRankingPeriod? = null) {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
if (period != null) {
|
||||
currentRankingPeriod = period
|
||||
}
|
||||
_isLoading.value = true
|
||||
|
||||
val requestPeriod = period ?: currentRankingPeriod
|
||||
compositeDisposable.add(
|
||||
repository.getCreatorProfileDonationRanking(
|
||||
id = userId,
|
||||
page = page,
|
||||
size = size,
|
||||
donationRankingPeriod = requestPeriod?.name,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -54,12 +64,18 @@ class UserProfileDonationAllViewModel(
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
it.data.donationRankingPeriod?.let { donationRankingPeriod ->
|
||||
_selectedDonationRankingPeriod.postValue(donationRankingPeriod)
|
||||
if (currentRankingPeriod == null) {
|
||||
currentRankingPeriod = donationRankingPeriod
|
||||
}
|
||||
}
|
||||
if (it.data.userDonationRanking.isNotEmpty()) {
|
||||
page += 1
|
||||
_donationLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
_donationLiveData.postValue(it.data!!)
|
||||
_isVisibleDonationRank.postValue(it.data.isVisibleDonationRank)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
@@ -87,10 +103,10 @@ class UserProfileDonationAllViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh(userId: Long) {
|
||||
fun refresh(userId: Long, period: DonationRankingPeriod? = null) {
|
||||
page = 1
|
||||
isLast = false
|
||||
getCreatorProfileDonationRanking(userId)
|
||||
getCreatorProfileDonationRanking(userId, period)
|
||||
}
|
||||
|
||||
fun onClickToggleVisibleDonationRank() {
|
||||
@@ -135,4 +151,45 @@ class UserProfileDonationAllViewModel(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun updateDonationRankingPeriod(period: DonationRankingPeriod) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
memberRepository.updateProfile(
|
||||
request = ProfileUpdateRequest(
|
||||
email = SharedPreferenceManager.email,
|
||||
donationRankingPeriod = period
|
||||
),
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success) {
|
||||
_selectedDonationRankingPeriod.postValue(period)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
@@ -14,7 +15,7 @@ import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.app.SodaLiveApp
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.main.DeepLinkActivity
|
||||
|
||||
class SodaFirebaseMessagingService : FirebaseMessagingService() {
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
@@ -62,33 +63,36 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val intent = Intent(this, SplashActivity::class.java)
|
||||
val intent = Intent(this, DeepLinkActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
|
||||
val roomId = messageData["room_id"]
|
||||
if (roomId != null) {
|
||||
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId.toLong())
|
||||
val deepLinkUrl = messageData["deepLink"] ?: messageData["deep_link"]
|
||||
if (!deepLinkUrl.isNullOrBlank()) {
|
||||
runCatching {
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
intent.data = Uri.parse(deepLinkUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val messageId = messageData["message_id"]
|
||||
if (messageId != null) {
|
||||
intent.putExtra(Constants.EXTRA_MESSAGE_ID, messageId.toLong())
|
||||
val deepLinkExtras = if (!deepLinkUrl.isNullOrBlank()) {
|
||||
android.os.Bundle().apply {
|
||||
putString("deep_link", deepLinkUrl)
|
||||
}
|
||||
} else {
|
||||
android.os.Bundle().apply {
|
||||
messageData["room_id"]?.let { putString("room_id", it) }
|
||||
messageData["message_id"]?.let { putString("message_id", it) }
|
||||
messageData["content_id"]?.let { putString("content_id", it) }
|
||||
messageData["channel_id"]?.let { putString("channel_id", it) }
|
||||
messageData["audition_id"]?.let { putString("audition_id", it) }
|
||||
messageData["deep_link_value"]?.let { putString("deep_link_value", it) }
|
||||
messageData["deep_link_sub5"]?.let { putString("deep_link_sub5", it) }
|
||||
}
|
||||
}
|
||||
|
||||
val audioContentId = messageData["content_id"]
|
||||
if (audioContentId != null) {
|
||||
intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId.toLong())
|
||||
}
|
||||
|
||||
val channelId = messageData["channel_id"]
|
||||
if (channelId != null) {
|
||||
intent.putExtra(Constants.EXTRA_USER_ID, channelId.toLong())
|
||||
}
|
||||
|
||||
val auditionId = messageData["audition_id"]
|
||||
if (auditionId != null) {
|
||||
intent.putExtra(Constants.EXTRA_AUDITION_ID, auditionId.toLong())
|
||||
if (!deepLinkExtras.isEmpty) {
|
||||
intent.putExtra(Constants.EXTRA_DATA, deepLinkExtras)
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
|
||||
@@ -44,6 +44,12 @@ class HomeContentThemeAdapter(
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setSelectedTheme(theme: String) {
|
||||
selectedTheme = theme
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemHomeContentThemeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -13,6 +12,9 @@ import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -45,6 +47,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentHomeBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||
@@ -68,6 +71,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
|
||||
@@ -95,34 +100,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_USER_ROLE) {
|
||||
if (
|
||||
sharedPreferences.getString(
|
||||
key,
|
||||
MemberRole.USER.name
|
||||
) == MemberRole.CREATOR.name
|
||||
) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.roleFlow.collect { role ->
|
||||
renderUploadContentByRole(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
setupView()
|
||||
bindData()
|
||||
|
||||
@@ -130,26 +118,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
}
|
||||
renderUploadContentByRole(SharedPreferenceManager.role)
|
||||
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
binding.llShortIcon.visibility = View.VISIBLE
|
||||
@@ -180,6 +155,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivPushNotification.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
PushNotificationListActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llShortIcon.visibility = View.GONE
|
||||
}
|
||||
@@ -199,9 +183,26 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
setupRecommendContent()
|
||||
}
|
||||
|
||||
private fun renderUploadContentByRole(role: String) {
|
||||
if (role == MemberRole.CREATOR.name) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setupLiveView() {
|
||||
liveAdapter = HomeLiveAdapter {
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
||||
val detailFragment = LiveRoomDetailFragment(
|
||||
it.roomId,
|
||||
onClickParticipant = { enterLiveRoom(it.roomId) },
|
||||
@@ -210,14 +211,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
onClickStart = {},
|
||||
onClickCancel = {}
|
||||
)
|
||||
if (detailFragment.isAdded) return@HomeLiveAdapter
|
||||
if (detailFragment.isAdded) return@ensureLoginAndAdultAuth
|
||||
|
||||
detailFragment.show(
|
||||
requireActivity().supportFragmentManager,
|
||||
detailFragment.tag
|
||||
)
|
||||
} else {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,6 +1357,30 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
onAuthed()
|
||||
}
|
||||
|
||||
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
return
|
||||
}
|
||||
|
||||
if (isAdult && !SharedPreferenceManager.isAuth) {
|
||||
SodaDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
desc = getString(R.string.auth_desc_live),
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startAuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
).show(screenWidth)
|
||||
return
|
||||
}
|
||||
|
||||
onAuthed()
|
||||
}
|
||||
|
||||
private fun startAuthFlow() {
|
||||
Auth.auth(requireActivity(), requireContext()) { json ->
|
||||
val bootpayResponse = Gson().fromJson(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetPushNotificationCategoryResponse(
|
||||
@SerializedName("categories") val categories: List<String>
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetPushNotificationListResponse(
|
||||
@SerializedName("totalCount") val totalCount: Long,
|
||||
@SerializedName("items") val items: List<PushNotificationListItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class PushNotificationListItem(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("senderNickname") val senderNickname: String,
|
||||
@SerializedName("senderProfileImage") val senderProfileImage: String?,
|
||||
@SerializedName("message") val message: String,
|
||||
@SerializedName("category") val category: String,
|
||||
@SerializedName("deepLink") val deepLink: String?,
|
||||
@SerializedName("sentAt") val sentAt: String
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityPushNotificationListBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||
import kr.co.vividnext.sodalive.settings.notification.NotificationReceiveSettingsActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBinding>(
|
||||
ActivityPushNotificationListBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: PushNotificationListViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var categoryAdapter: HomeContentThemeAdapter
|
||||
private lateinit var notificationAdapter: PushNotificationListAdapter
|
||||
private var isInitialCategorySelected = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
bindData()
|
||||
viewModel.getPushNotificationCategories()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.tvBack.text = getString(R.string.screen_push_notification_title)
|
||||
binding.tvBack.setOnClickListener { finish() }
|
||||
binding.ivSettings.setOnClickListener {
|
||||
startActivity(Intent(this, NotificationReceiveSettingsActivity::class.java))
|
||||
}
|
||||
|
||||
setupCategoryList()
|
||||
setupNotificationList()
|
||||
}
|
||||
|
||||
private fun setupCategoryList() {
|
||||
categoryAdapter = HomeContentThemeAdapter("") { selectedCategory ->
|
||||
viewModel.selectCategory(selectedCategory)
|
||||
}
|
||||
|
||||
binding.rvCategory.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvCategory.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
categoryAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvCategory.adapter = categoryAdapter
|
||||
}
|
||||
|
||||
private fun setupNotificationList() {
|
||||
notificationAdapter = PushNotificationListAdapter(
|
||||
onClickItem = { openDeepLink(it.deepLink) }
|
||||
)
|
||||
|
||||
binding.rvNotification.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvNotification.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
if (dy <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!recyclerView.canScrollVertically(1)) {
|
||||
viewModel.getPushNotificationList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvNotification.adapter = notificationAdapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { message ->
|
||||
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.categoryListLiveData.observe(this) { categories ->
|
||||
categoryAdapter.addItems(categories)
|
||||
|
||||
if (!isInitialCategorySelected && categories.isNotEmpty()) {
|
||||
val initialCategory = categories.first()
|
||||
categoryAdapter.setSelectedTheme(initialCategory)
|
||||
viewModel.selectCategory(initialCategory)
|
||||
isInitialCategorySelected = true
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.notificationListLiveData.observe(this) { items ->
|
||||
notificationAdapter.submitItems(items)
|
||||
binding.llEmpty.visibility = if (items.isEmpty()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDeepLink(deepLink: String?) {
|
||||
val deepLinkUrl = deepLink?.takeIf { it.isNotBlank() } ?: return
|
||||
|
||||
runCatching {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)))
|
||||
}.onFailure {
|
||||
Toast.makeText(applicationContext, getString(R.string.common_error_unknown), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPushNotificationListBinding
|
||||
|
||||
class PushNotificationListAdapter(
|
||||
private val onClickItem: (PushNotificationListItem) -> Unit
|
||||
) : RecyclerView.Adapter<PushNotificationListAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<PushNotificationListItem>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemPushNotificationListBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: PushNotificationListItem) {
|
||||
binding.ivProfile.load(item.senderProfileImage) {
|
||||
transformations(CircleCropTransformation())
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
error(R.drawable.ic_place_holder)
|
||||
crossfade(true)
|
||||
}
|
||||
binding.tvNickname.text = item.senderNickname
|
||||
binding.tvTimeAgo.text = item.relativeTimeText(binding.root.context)
|
||||
binding.tvMessage.text = item.message
|
||||
binding.root.setOnClickListener { onClickItem(item) }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun submitItems(items: List<PushNotificationListItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemPushNotificationListBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.user.UserRepository
|
||||
|
||||
class PushNotificationListViewModel(
|
||||
private val userRepository: UserRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private val _categoryListLiveData = MutableLiveData<List<String>>()
|
||||
val categoryListLiveData: LiveData<List<String>>
|
||||
get() = _categoryListLiveData
|
||||
|
||||
private val _notificationListLiveData = MutableLiveData<List<PushNotificationListItem>>()
|
||||
val notificationListLiveData: LiveData<List<PushNotificationListItem>>
|
||||
get() = _notificationListLiveData
|
||||
|
||||
private var page = 1
|
||||
private val size = 20
|
||||
private var totalCount = 0L
|
||||
private var selectedCategory: String? = null
|
||||
private var isLastPage = false
|
||||
private val loadedItems = mutableListOf<PushNotificationListItem>()
|
||||
|
||||
fun getPushNotificationCategories() {
|
||||
val unknownError = kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
|
||||
compositeDisposable.add(
|
||||
userRepository.getPushNotificationCategories(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_categoryListLiveData.value = it.data.categories
|
||||
} else {
|
||||
_toastLiveData.value = it.message ?: unknownError
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.value = unknownError
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun selectCategory(category: String?) {
|
||||
val normalizedCategory = category?.takeIf { it.isNotBlank() }
|
||||
if (selectedCategory == normalizedCategory && loadedItems.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedCategory = normalizedCategory
|
||||
resetPaging()
|
||||
getPushNotificationList()
|
||||
}
|
||||
|
||||
fun getPushNotificationList() {
|
||||
if (isLastPage || _isLoading.value == true) {
|
||||
return
|
||||
}
|
||||
|
||||
_isLoading.value = true
|
||||
val unknownError = kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
|
||||
compositeDisposable.add(
|
||||
userRepository.getPushNotificationList(
|
||||
page = page,
|
||||
size = size,
|
||||
category = selectedCategory,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
|
||||
if (it.success && it.data != null) {
|
||||
totalCount = it.data.totalCount
|
||||
loadedItems.addAll(it.data.items)
|
||||
_notificationListLiveData.value = loadedItems.toList()
|
||||
|
||||
isLastPage =
|
||||
it.data.items.isEmpty() ||
|
||||
totalCount == 0L ||
|
||||
loadedItems.size.toLong() >= totalCount
|
||||
|
||||
if (!isLastPage) {
|
||||
page += 1
|
||||
}
|
||||
} else {
|
||||
_toastLiveData.value = it.message ?: unknownError
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.value = unknownError
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun resetPaging() {
|
||||
page = 1
|
||||
totalCount = 0L
|
||||
isLastPage = false
|
||||
loadedItems.clear()
|
||||
_notificationListLiveData.value = emptyList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package kr.co.vividnext.sodalive.home.pushnotification
|
||||
|
||||
import android.content.Context
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
fun PushNotificationListItem.relativeTimeText(context: Context): String {
|
||||
val pastMillis = parsePushNotificationUtcToMillis(sentAt)
|
||||
?: return context.getString(R.string.character_comment_time_just_now)
|
||||
|
||||
val diff = (System.currentTimeMillis() - pastMillis).coerceAtLeast(0L)
|
||||
val minute = 60_000L
|
||||
val hour = 60 * minute
|
||||
val day = 24 * hour
|
||||
|
||||
return when {
|
||||
diff < minute -> context.getString(R.string.character_comment_time_just_now)
|
||||
diff < hour -> context.getString(R.string.character_comment_time_minutes, (diff / minute).toInt())
|
||||
diff < day -> context.getString(R.string.character_comment_time_hours, (diff / hour).toInt())
|
||||
else -> context.getString(R.string.character_comment_time_days, (diff / day).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun parsePushNotificationUtcToMillis(sentAt: String?): Long? {
|
||||
if (sentAt.isNullOrBlank()) return null
|
||||
|
||||
val value = sentAt.trim()
|
||||
if (value.all { it.isDigit() }) {
|
||||
return try {
|
||||
val epoch = value.toLong()
|
||||
if (value.length <= 10) epoch * 1000 else epoch
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val patterns = listOf(
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssX",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy/MM/dd HH:mm:ss"
|
||||
)
|
||||
|
||||
for (pattern in patterns) {
|
||||
try {
|
||||
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsed: Date? = dateFormat.parse(value)
|
||||
if (parsed != null) {
|
||||
return parsed.time
|
||||
}
|
||||
} catch (_: ParseException) {
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -8,5 +8,6 @@ data class GetLatestFinishedLiveResponse(
|
||||
@SerializedName("memberId") val memberId: Long,
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String,
|
||||
@SerializedName("timeAgo") val timeAgo: String
|
||||
@SerializedName("timeAgo") val timeAgo: String,
|
||||
@SerializedName("dateUtc") val dateUtc: String
|
||||
)
|
||||
|
||||
@@ -5,7 +5,14 @@ import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemLatestFinishedLiveBinding
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class LatestFinishedLiveAdapter(
|
||||
private val onClick: (Long) -> Unit
|
||||
@@ -49,8 +56,87 @@ class LatestFinishedLiveAdapter(
|
||||
.into(binding.ivProfile)
|
||||
|
||||
binding.tvNickname.text = item.nickname
|
||||
binding.tvTimeAgo.text = item.timeAgo
|
||||
binding.tvTimeAgo.text = relativeTimeText(item)
|
||||
binding.root.setOnClickListener { onClick(item.memberId) }
|
||||
}
|
||||
|
||||
private fun relativeTimeText(item: GetLatestFinishedLiveResponse): String {
|
||||
val pastMillis = parseDateUtcToMillis(item.dateUtc)
|
||||
if (pastMillis == null) {
|
||||
return item.timeAgo
|
||||
}
|
||||
|
||||
val timezone = TimeZone.getDefault()
|
||||
val nowCalendar = Calendar.getInstance(timezone, Locale.getDefault())
|
||||
val pastCalendar = Calendar.getInstance(timezone, Locale.getDefault()).apply {
|
||||
timeInMillis = pastMillis
|
||||
}
|
||||
|
||||
val minute = 60_000L
|
||||
val hour = 60 * minute
|
||||
val day = 24 * hour
|
||||
|
||||
val diff = (nowCalendar.timeInMillis - pastCalendar.timeInMillis).coerceAtLeast(0L)
|
||||
|
||||
return when {
|
||||
diff < minute -> context.getString(R.string.latest_finished_live_time_just_now)
|
||||
diff < hour -> {
|
||||
val minutes = (diff / minute).toInt()
|
||||
context.getString(R.string.latest_finished_live_time_minutes, minutes)
|
||||
}
|
||||
|
||||
diff < day -> {
|
||||
val hours = (diff / hour).toInt()
|
||||
context.getString(R.string.latest_finished_live_time_hours, hours)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val days = (diff / day).toInt()
|
||||
context.getString(R.string.latest_finished_live_time_days, days)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDateUtcToMillis(dateUtc: String?): Long? {
|
||||
if (dateUtc.isNullOrBlank()) return null
|
||||
|
||||
val value = dateUtc.trim()
|
||||
if (value.all { it.isDigit() }) {
|
||||
return try {
|
||||
val epoch = value.toLong()
|
||||
if (value.length <= 10) epoch * 1000 else epoch
|
||||
} catch (exception: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val patterns = listOf(
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssX",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy/MM/dd HH:mm:ss"
|
||||
)
|
||||
|
||||
for (pattern in patterns) {
|
||||
try {
|
||||
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsed: Date? = dateFormat.parse(value)
|
||||
if (parsed != null) {
|
||||
return parsed.time
|
||||
}
|
||||
} catch (exception: ParseException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,25 @@ package kr.co.vividnext.sodalive.live
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.zhpan.bannerview.BaseBannerAdapter
|
||||
import com.zhpan.indicator.enums.IndicatorSlideMode
|
||||
import com.zhpan.indicator.enums.IndicatorStyle
|
||||
@@ -26,6 +30,7 @@ import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
@@ -54,21 +59,29 @@ import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
|
||||
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.auth.Auth
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.search.SearchActivity
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@UnstableApi
|
||||
class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::inflate) {
|
||||
private val viewModel: LiveViewModel by inject()
|
||||
private val myPageViewModel: MyPageViewModel by inject()
|
||||
|
||||
private lateinit var liveNowAdapter: LiveNowAdapter
|
||||
private lateinit var liveReservationAdapter: LiveReservationAdapter
|
||||
@@ -81,27 +94,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
private var message = ""
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_USER_ROLE) {
|
||||
if (
|
||||
sharedPreferences.getString(
|
||||
key,
|
||||
MemberRole.USER.name
|
||||
) == MemberRole.CREATOR.name
|
||||
) {
|
||||
binding.llMakeLive.visibility = View.VISIBLE
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
} else {
|
||||
binding.llMakeLive.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -122,7 +114,14 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.roleFlow.collect { role ->
|
||||
renderMakeLiveByRole(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupView()
|
||||
|
||||
@@ -131,7 +130,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -150,17 +148,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
}
|
||||
|
||||
binding.llMakeLive.visibility =
|
||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
renderMakeLiveByRole(SharedPreferenceManager.role)
|
||||
|
||||
setupToolbar()
|
||||
setupLiveNow()
|
||||
@@ -172,6 +160,19 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
setupLiveReservation()
|
||||
}
|
||||
|
||||
private fun renderMakeLiveByRole(role: String) {
|
||||
if (role == MemberRole.CREATOR.name) {
|
||||
binding.llMakeLive.visibility = View.VISIBLE
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
binding.llMakeLive.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
binding.llShortIcon.visibility = View.VISIBLE
|
||||
@@ -510,7 +511,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
.rvLiveNow
|
||||
|
||||
liveNowAdapter = LiveNowAdapter {
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
||||
val detailFragment = LiveRoomDetailFragment(
|
||||
it.roomId,
|
||||
onClickParticipant = { enterLiveRoom(it.roomId) },
|
||||
@@ -519,14 +520,12 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
onClickStart = {},
|
||||
onClickCancel = {}
|
||||
)
|
||||
if (detailFragment.isAdded) return@LiveNowAdapter
|
||||
if (detailFragment.isAdded) return@ensureLoginAndAdultAuth
|
||||
|
||||
detailFragment.show(
|
||||
requireActivity().supportFragmentManager,
|
||||
detailFragment.tag
|
||||
)
|
||||
} else {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +856,56 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
return
|
||||
}
|
||||
|
||||
if (isAdult && !SharedPreferenceManager.isAuth) {
|
||||
SodaDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
desc = getString(R.string.auth_desc_live),
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startAuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
).show(screenWidth)
|
||||
return
|
||||
}
|
||||
|
||||
onAuthed()
|
||||
}
|
||||
|
||||
private fun startAuthFlow() {
|
||||
Auth.auth(requireActivity(), requireContext()) { json ->
|
||||
val bootpayResponse = Gson().fromJson(
|
||||
json,
|
||||
BootpayResponse::class.java
|
||||
)
|
||||
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
|
||||
requireActivity().runOnUiThread {
|
||||
myPageViewModel.authVerify(request) {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
SplashActivity::class.java
|
||||
).apply {
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
)
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun enterLiveRoom(roomId: Long) {
|
||||
requireContext().startService(
|
||||
@@ -880,7 +929,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
|
||||
viewModel.getRoomDetail(roomId) {
|
||||
if (it.channelName != null) {
|
||||
if (!it.channelName.isNullOrBlank()) {
|
||||
if (it.manager.id == SharedPreferenceManager.userId) {
|
||||
handler.postDelayed({
|
||||
viewModel.enterRoom(roomId, onEnterRoomSuccess)
|
||||
|
||||
@@ -200,9 +200,13 @@ class LiveRepository(
|
||||
|
||||
fun creatorFollow(
|
||||
creatorId: Long,
|
||||
notify: Boolean = true,
|
||||
token: String
|
||||
) = userApi.creatorFollow(
|
||||
request = CreatorFollowRequestRequest(creatorId = creatorId),
|
||||
request = CreatorFollowRequestRequest(
|
||||
creatorId = creatorId,
|
||||
isNotify = notify
|
||||
),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ class LiveNowAdapter(
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.ivShield.visibility = if (item.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (item.price > 0) {
|
||||
binding.llCan.visibility = View.VISIBLE
|
||||
binding.tvCan.text = item.price.moneyFormat()
|
||||
@@ -54,8 +60,31 @@ class LiveNowAdapter(
|
||||
binding.tvFree.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
bindTags(item)
|
||||
|
||||
binding.root.setOnClickListener { onClick(item) }
|
||||
}
|
||||
|
||||
private fun bindTags(item: GetRoomListResponse) {
|
||||
val tags = item.tags.filter { it.isNotBlank() }.take(2)
|
||||
if (tags.isEmpty()) {
|
||||
binding.llTags.visibility = View.GONE
|
||||
binding.tvTag1.visibility = View.GONE
|
||||
binding.tvTag2.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
binding.llTags.visibility = View.VISIBLE
|
||||
binding.tvTag1.text = tags[0]
|
||||
binding.tvTag1.visibility = View.VISIBLE
|
||||
|
||||
if (tags.size > 1) {
|
||||
binding.tvTag2.text = tags[1]
|
||||
binding.tvTag2.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvTag2.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
|
||||
@@ -3,28 +3,38 @@ package kr.co.vividnext.sodalive.live.now.all
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityLiveNowAllBinding
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
|
||||
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
|
||||
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
|
||||
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.auth.Auth
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -36,6 +46,7 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
|
||||
ActivityLiveNowAllBinding::inflate
|
||||
) {
|
||||
private val viewModel: LiveViewModel by inject()
|
||||
private val myPageViewModel: MyPageViewModel by inject()
|
||||
|
||||
private lateinit var adapter: LiveNowAllAdapter
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
@@ -53,22 +64,24 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
val spanCount = 2
|
||||
val spacing = 48
|
||||
val spacing = 16.dpToPx().toInt()
|
||||
val recyclerView = binding.rvLive
|
||||
adapter = LiveNowAllAdapter(itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 2) {
|
||||
val detailFragment = LiveRoomDetailFragment(
|
||||
it.roomId,
|
||||
onClickParticipant = { enterLiveRoom(it.roomId) },
|
||||
onClickReservation = {},
|
||||
onClickModify = {},
|
||||
onClickStart = {},
|
||||
onClickCancel = {}
|
||||
)
|
||||
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
||||
val detailFragment = LiveRoomDetailFragment(
|
||||
it.roomId,
|
||||
onClickParticipant = { enterLiveRoom(it.roomId) },
|
||||
onClickReservation = {},
|
||||
onClickModify = {},
|
||||
onClickStart = {},
|
||||
onClickCancel = {}
|
||||
)
|
||||
|
||||
detailFragment.show(
|
||||
supportFragmentManager,
|
||||
detailFragment.tag
|
||||
)
|
||||
detailFragment.show(
|
||||
supportFragmentManager,
|
||||
detailFragment.tag
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
|
||||
@@ -100,6 +113,68 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
showLoginActivity()
|
||||
return
|
||||
}
|
||||
|
||||
if (isAdult && !SharedPreferenceManager.isAuth) {
|
||||
SodaDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
desc = getString(R.string.auth_desc_live),
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startAuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
).show(screenWidth)
|
||||
return
|
||||
}
|
||||
|
||||
onAuthed()
|
||||
}
|
||||
|
||||
private fun showLoginActivity() {
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
startActivity(
|
||||
Intent(applicationContext, LoginActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_DATA, intent.extras)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthFlow() {
|
||||
Auth.auth(this, applicationContext) { json ->
|
||||
val bootpayResponse = Gson().fromJson(
|
||||
json,
|
||||
BootpayResponse::class.java
|
||||
)
|
||||
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
|
||||
runOnUiThread {
|
||||
myPageViewModel.authVerify(request) {
|
||||
startActivity(
|
||||
Intent(
|
||||
applicationContext,
|
||||
SplashActivity::class.java
|
||||
).apply {
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterLiveRoom(roomId: Long) {
|
||||
startService(
|
||||
Intent(applicationContext, AudioContentPlayService::class.java).apply {
|
||||
|
||||
@@ -2,26 +2,18 @@ package kr.co.vividnext.sodalive.live.now.all
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemLiveNowAllBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemLiveNowBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.live.GetRoomListResponse
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class LiveNowAllAdapter(
|
||||
private val itemWidth: Int,
|
||||
@@ -32,25 +24,43 @@ class LiveNowAllAdapter(
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemLiveNowAllBinding
|
||||
private val binding: ItemLiveNowBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: GetRoomListResponse) {
|
||||
val baseCardWidth = 144.dpToPx()
|
||||
val baseCardHeight = 204.dpToPx()
|
||||
val scale = itemWidth / baseCardWidth
|
||||
|
||||
val rootLayoutParams = binding.root.layoutParams
|
||||
rootLayoutParams.width = itemWidth
|
||||
rootLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
binding.root.layoutParams = rootLayoutParams
|
||||
|
||||
val cardLayoutParams = binding.cardLiveNow.layoutParams
|
||||
cardLayoutParams.width = itemWidth
|
||||
cardLayoutParams.height = (baseCardHeight * scale).roundToInt()
|
||||
binding.cardLiveNow.layoutParams = cardLayoutParams
|
||||
|
||||
val profileLayoutParams = binding.flProfile.layoutParams
|
||||
profileLayoutParams.width = (84.dpToPx() * scale).roundToInt()
|
||||
profileLayoutParams.height = (84.dpToPx() * scale).roundToInt()
|
||||
binding.flProfile.layoutParams = profileLayoutParams
|
||||
|
||||
val profileImageLayoutParams = binding.ivProfile.layoutParams
|
||||
profileImageLayoutParams.width = (72.dpToPx() * scale).roundToInt()
|
||||
profileImageLayoutParams.height = (72.dpToPx() * scale).roundToInt()
|
||||
binding.ivProfile.layoutParams = profileImageLayoutParams
|
||||
|
||||
Glide
|
||||
.with(context)
|
||||
.load(item.coverImageUrl)
|
||||
.load(item.creatorProfileImage)
|
||||
.apply(
|
||||
RequestOptions().transform(
|
||||
CenterCrop(),
|
||||
RoundedCorners(14)
|
||||
CircleCrop()
|
||||
)
|
||||
)
|
||||
.into(binding.ivCover)
|
||||
|
||||
val layoutParams = binding.ivCover
|
||||
.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.width = itemWidth
|
||||
layoutParams.height = itemWidth * 144 / 102
|
||||
.into(binding.ivProfile)
|
||||
|
||||
binding.ivLock.visibility = if (item.isPrivateRoom) {
|
||||
View.VISIBLE
|
||||
@@ -58,61 +68,53 @@ class LiveNowAllAdapter(
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (item.price > 0) {
|
||||
binding.tvPrice.text = "${item.price}"
|
||||
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
|
||||
R.drawable.ic_can_white,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_dd4500)
|
||||
binding.ivShield.visibility = if (item.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
binding.tvPrice.text = context.getString(R.string.screen_live_now_all_free)
|
||||
binding.tvPrice.setCompoundDrawables(null, null, null, null)
|
||||
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_111111)
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (item.tags.isNotEmpty()) {
|
||||
binding.tvTags.visibility = View.VISIBLE
|
||||
binding.tvTags.text = item.tags.joinToString(" ") { "#$it" }
|
||||
if (item.price > 0) {
|
||||
binding.llCan.visibility = View.VISIBLE
|
||||
binding.tvCan.text = item.price.moneyFormat()
|
||||
binding.tvFree.visibility = View.GONE
|
||||
} else {
|
||||
binding.tvTags.visibility = View.GONE
|
||||
binding.llCan.visibility = View.GONE
|
||||
binding.tvFree.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvNickname.text = item.creatorNickname
|
||||
binding.ivProfile.loadUrl(item.creatorProfileImage) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
if (item.numberOfPeople - item.numberOfParticipate <= 2) {
|
||||
binding.llRemainingParticipant.visibility = View.VISIBLE
|
||||
if (item.numberOfPeople > item.numberOfParticipate) {
|
||||
binding.tvRemainingParticipantNumber.visibility = View.VISIBLE
|
||||
binding.tvRemainingParticipant.text =
|
||||
context.getString(R.string.screen_live_now_all_remaining)
|
||||
binding.tvRemainingParticipantNumber.text =
|
||||
"${item.numberOfPeople - item.numberOfParticipate}"
|
||||
} else {
|
||||
binding.tvRemainingParticipantNumber.visibility = View.GONE
|
||||
binding.tvRemainingParticipant.text =
|
||||
context.getString(R.string.screen_live_now_all_sold_out)
|
||||
binding.tvRemainingParticipantNumber.text = ""
|
||||
}
|
||||
} else {
|
||||
binding.llRemainingParticipant.visibility = View.GONE
|
||||
}
|
||||
bindTags(item)
|
||||
|
||||
binding.root.setOnClickListener { onClick(item) }
|
||||
}
|
||||
|
||||
private fun bindTags(item: GetRoomListResponse) {
|
||||
val tags = item.tags.filter { it.isNotBlank() }.take(2)
|
||||
if (tags.isEmpty()) {
|
||||
binding.llTags.visibility = View.GONE
|
||||
binding.tvTag1.visibility = View.GONE
|
||||
binding.tvTag2.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
binding.llTags.visibility = View.VISIBLE
|
||||
binding.tvTag1.text = tags[0]
|
||||
binding.tvTag1.visibility = View.VISIBLE
|
||||
|
||||
if (tags.size > 1) {
|
||||
binding.tvTag2.text = tags[1]
|
||||
binding.tvTag2.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvTag2.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
parent.context,
|
||||
ItemLiveNowAllBinding.inflate(
|
||||
ItemLiveNowBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
enum class GenderRestriction {
|
||||
@SerializedName("ALL") ALL,
|
||||
@SerializedName("MALE_ONLY") MALE_ONLY,
|
||||
@SerializedName("FEMALE_ONLY") FEMALE_ONLY
|
||||
}
|
||||
@@ -2,14 +2,18 @@ package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
@@ -30,9 +34,11 @@ import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
@@ -47,6 +53,9 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.CircleCropTransformation
|
||||
@@ -80,6 +89,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveService
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomBinding
|
||||
import kr.co.vividnext.sodalive.dialog.LiveDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
@@ -104,11 +114,15 @@ import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
|
||||
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
|
||||
import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog
|
||||
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
|
||||
import kr.co.vividnext.sodalive.main.DeepLinkActivity
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import kr.co.vividnext.sodalive.report.UserReportDialog
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.random.Random
|
||||
@@ -133,6 +147,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
showLiveRoomUserProfileDialog(userId = userId)
|
||||
}
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private var rvChatBaseBottomMargin: Int? = null
|
||||
|
||||
private lateinit var agora: Agora
|
||||
private lateinit var roomDialog: LiveRoomDialog
|
||||
@@ -149,10 +164,30 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
private var isAvailableLikeHeart = false
|
||||
private var buttonPosition = IntArray(2)
|
||||
private var isEntryMessageEnabled = true
|
||||
private var isCreatorFollowNotifyEnabled = true
|
||||
|
||||
// joinChannel 중복 호출 방지 플래그
|
||||
private var hasInvokedJoinChannel = false
|
||||
|
||||
private var v2vSourceLanguage: String? = null
|
||||
private var v2vTargetLanguage: String? = null
|
||||
private var isV2vAvailable = false
|
||||
private val v2vMessageCache = mutableMapOf<String, MutableList<V2vMessageChunk>>()
|
||||
private val v2vMessageCacheTimestamps = mutableMapOf<String, Long>()
|
||||
|
||||
private data class V2vMessageChunk(
|
||||
val partIdx: Int,
|
||||
val partSum: Int,
|
||||
val content: String
|
||||
)
|
||||
|
||||
private data class V2vChunkEnvelope(
|
||||
val messageId: String,
|
||||
val partIdx: Int,
|
||||
val partSum: Int,
|
||||
val content: String
|
||||
)
|
||||
|
||||
// region 채팅 금지
|
||||
private var isNoChatting = false
|
||||
private var remainingNoChattingTime = NO_CHATTING_TIME
|
||||
@@ -214,11 +249,27 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
private val deepLinkConfirmReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val bundle = intent?.getBundleExtra(Constants.EXTRA_DATA) ?: return
|
||||
|
||||
showDeepLinkNavigationDialog {
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(Constants.EXTRA_DATA, bundle)
|
||||
}
|
||||
startActivity(nextIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initAgora()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
applyKeyboardPanInsets()
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
this.roomId = intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0)
|
||||
@@ -236,6 +287,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isForeground = true
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
deepLinkConfirmReceiver,
|
||||
IntentFilter(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM)
|
||||
)
|
||||
|
||||
if (this::layoutManager.isInitialized) {
|
||||
layoutManager.scrollToPosition(chatAdapter.itemCount - 1)
|
||||
@@ -255,10 +311,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(deepLinkConfirmReceiver)
|
||||
isForeground = false
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cropper.cleanup()
|
||||
hideKeyboard {
|
||||
viewModel.quitRoom(roomId) {
|
||||
viewModel.stopV2vTranslation()
|
||||
SodaLiveService.stopService(this)
|
||||
agora.deInitAgoraEngine(rtmEventListener)
|
||||
RtmClient.release()
|
||||
@@ -267,6 +330,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
countDownTimer.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region setupView
|
||||
@@ -512,6 +576,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
|
||||
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
|
||||
binding.tvSignatureSwitch.setOnClickListener { viewModel.toggleSignatureImage() }
|
||||
binding.tvV2vSignatureSwitch.setOnClickListener { toggleV2vCaption() }
|
||||
binding.llDonation.setOnClickListener {
|
||||
LiveRoomDonationRankingDialog(
|
||||
activity = this,
|
||||
@@ -533,6 +598,24 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
// endregion
|
||||
|
||||
private fun applyKeyboardPanInsets() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
|
||||
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
v.translationY = if (imeVisible) -ime.bottom.toFloat() else 0f
|
||||
|
||||
insets
|
||||
}
|
||||
ViewCompat.requestApplyInsets(binding.root)
|
||||
}
|
||||
|
||||
private fun secondToMillis(second: Float): Long {
|
||||
return (second * 1000).toLong()
|
||||
}
|
||||
@@ -647,6 +730,98 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindCreatorFollowButton(response: GetRoomInfoResponse) {
|
||||
val isMyRoom = response.creatorId == SharedPreferenceManager.userId
|
||||
|
||||
if (isMyRoom) {
|
||||
binding.ivCreatorFollow.visibility = View.GONE
|
||||
binding.ivCreatorFollow.setOnClickListener(null)
|
||||
binding.llViewUsers.visibility = View.VISIBLE
|
||||
binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
|
||||
isCreatorFollowNotifyEnabled = true
|
||||
return
|
||||
}
|
||||
|
||||
binding.llViewUsers.visibility = View.GONE
|
||||
binding.llViewUsers.setOnClickListener(null)
|
||||
binding.ivCreatorFollow.visibility = View.VISIBLE
|
||||
|
||||
if (!response.isFollowing) {
|
||||
isCreatorFollowNotifyEnabled = true
|
||||
}
|
||||
|
||||
val followTextRes = if (response.isFollowing) {
|
||||
R.string.screen_home_following
|
||||
} else {
|
||||
R.string.screen_home_follow
|
||||
}
|
||||
val followIconRes = if (response.isFollowing) {
|
||||
if (isCreatorFollowNotifyEnabled) {
|
||||
R.drawable.ic_live_creator_follow_alarm
|
||||
} else {
|
||||
R.drawable.ic_live_creator_follow_no_alarm
|
||||
}
|
||||
} else {
|
||||
R.drawable.ic_live_creator_follow_plus
|
||||
}
|
||||
|
||||
binding.tvCreatorFollow.text = getString(followTextRes)
|
||||
binding.ivCreatorFollowIcon.setImageResource(followIconRes)
|
||||
|
||||
binding.ivCreatorFollow.setOnClickListener {
|
||||
if (response.isFollowing) {
|
||||
showCreatorFollowNotifyDialog(response.creatorId)
|
||||
} else {
|
||||
isCreatorFollowNotifyEnabled = true
|
||||
viewModel.creatorFollow(
|
||||
creatorId = response.creatorId,
|
||||
roomId = roomId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCreatorFollowNotifyDialog(creatorId: Long) {
|
||||
if (
|
||||
supportFragmentManager.findFragmentByTag(
|
||||
CreatorFollowNotifyFragment::class.java.simpleName
|
||||
) != null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val notifyFragment = CreatorFollowNotifyFragment(
|
||||
onClickNotifyAll = {
|
||||
isCreatorFollowNotifyEnabled = true
|
||||
viewModel.creatorFollow(
|
||||
creatorId = creatorId,
|
||||
roomId = roomId,
|
||||
notify = true
|
||||
)
|
||||
},
|
||||
onClickNotifyNone = {
|
||||
isCreatorFollowNotifyEnabled = false
|
||||
viewModel.creatorFollow(
|
||||
creatorId = creatorId,
|
||||
roomId = roomId,
|
||||
notify = false
|
||||
)
|
||||
},
|
||||
onClickUnFollow = {
|
||||
isCreatorFollowNotifyEnabled = true
|
||||
viewModel.creatorUnFollow(
|
||||
creatorId = creatorId,
|
||||
roomId = roomId
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
notifyFragment.show(
|
||||
supportFragmentManager,
|
||||
CreatorFollowNotifyFragment::class.java.simpleName
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun bindData() {
|
||||
viewModel.isBgOn.observe(this) {
|
||||
@@ -702,6 +877,37 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isV2vCaptionOn.observe(this) { isOn ->
|
||||
if (isOn) {
|
||||
binding.tvV2vSignatureSwitch.text =
|
||||
getString(R.string.screen_live_room_v2v_signature_on_label)
|
||||
binding.tvV2vSignatureSwitch.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.tvV2vSignatureSwitch
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_3bb9f1)
|
||||
binding.tvV2vCaption.visibility = View.VISIBLE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
} else {
|
||||
binding.tvV2vSignatureSwitch.text =
|
||||
getString(R.string.screen_live_room_v2v_signature_off_label)
|
||||
binding.tvV2vSignatureSwitch.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.tvV2vSignatureSwitch
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_bbbbbb)
|
||||
binding.tvV2vCaption.text = ""
|
||||
binding.tvV2vCaption.visibility = View.GONE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
@@ -742,7 +948,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
|
||||
viewModel.roomInfoLiveData.observe(this) { response ->
|
||||
binding.tv19.visibility = if (response.isAdult) {
|
||||
updateV2vAvailability(response)
|
||||
binding.ivShield.visibility = if (response.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
@@ -879,12 +1086,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
if (response.creatorId == SharedPreferenceManager.userId) {
|
||||
binding.llViewUsers.visibility = View.VISIBLE
|
||||
binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
|
||||
} else {
|
||||
binding.llViewUsers.visibility = View.GONE
|
||||
}
|
||||
bindCreatorFollowButton(response)
|
||||
|
||||
binding.tvParticipate.text = "${response.participantsCount}"
|
||||
setNoticeAndClickableUrl(binding.tvNotice, response.notice)
|
||||
@@ -902,29 +1104,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
if (response.creatorId != SharedPreferenceManager.userId) {
|
||||
binding.ivCreatorFollow.visibility = View.VISIBLE
|
||||
if (response.isFollowing) {
|
||||
binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
|
||||
binding.ivCreatorFollow.setOnClickListener {
|
||||
viewModel.creatorUnFollow(
|
||||
creatorId = response.creatorId,
|
||||
roomId = roomId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
|
||||
binding.ivCreatorFollow.setOnClickListener {
|
||||
viewModel.creatorFollow(
|
||||
creatorId = response.creatorId,
|
||||
roomId = roomId
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.ivCreatorFollow.visibility = View.GONE
|
||||
}
|
||||
|
||||
isHost = response.creatorId == SharedPreferenceManager.userId
|
||||
initLikeHeartButton()
|
||||
initRouletteSettingButton()
|
||||
@@ -1032,7 +1211,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val url = spannable.subSequence(start, end).toString()
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
handleNoticeUrlClick(url)
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
@@ -1042,6 +1221,55 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun handleNoticeUrlClick(url: String) {
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
if (isInAppDeepLinkIntent(viewIntent)) {
|
||||
showDeepLinkNavigationDialog {
|
||||
startActivity(viewIntent)
|
||||
finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
startActivity(viewIntent)
|
||||
}
|
||||
|
||||
private fun isInAppDeepLinkIntent(intent: Intent): Boolean {
|
||||
val handlers = queryIntentActivitiesCompat(intent)
|
||||
return handlers.any { resolveInfo ->
|
||||
resolveInfo.activityInfo.packageName == packageName &&
|
||||
resolveInfo.activityInfo.name == DeepLinkActivity::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun queryIntentActivitiesCompat(intent: Intent): List<ResolveInfo> {
|
||||
val flags = PackageManager.MATCH_DEFAULT_ONLY
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.ResolveInfoFlags.of(flags.toLong())
|
||||
)
|
||||
} else {
|
||||
packageManager.queryIntentActivities(intent, flags)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeepLinkNavigationDialog(onConfirm: () -> Unit) {
|
||||
SodaDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_live_room_deeplink_move_title),
|
||||
desc = getString(R.string.screen_live_room_deeplink_move_message),
|
||||
confirmButtonTitle = getString(R.string.screen_live_room_yes),
|
||||
confirmButtonClick = {
|
||||
onConfirm()
|
||||
},
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun onClickQuit() {
|
||||
hideKeyboard {
|
||||
if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) {
|
||||
@@ -1103,6 +1331,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
})
|
||||
rvChat.adapter = chatAdapter
|
||||
setupV2vCaptionOffset()
|
||||
rvChat.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!rvChat.canScrollVertically(1)) {
|
||||
binding.tvNewChat.visibility = View.GONE
|
||||
@@ -1140,6 +1369,33 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
rvSpeakers.adapter = speakerListAdapter
|
||||
}
|
||||
|
||||
private fun setupV2vCaptionOffset() {
|
||||
binding.tvV2vCaption.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
|
||||
private fun updateChatBottomMarginByV2vCaption() {
|
||||
val rvChat = binding.rvChat
|
||||
val layoutParams = rvChat.layoutParams as? ViewGroup.MarginLayoutParams ?: return
|
||||
val baseMargin = rvChatBaseBottomMargin ?: layoutParams.bottomMargin.also {
|
||||
rvChatBaseBottomMargin = it
|
||||
}
|
||||
|
||||
val captionHeight = if (binding.tvV2vCaption.visibility == View.VISIBLE) {
|
||||
binding.tvV2vCaption.height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val targetBottomMargin = baseMargin + captionHeight
|
||||
|
||||
if (layoutParams.bottomMargin != targetBottomMargin) {
|
||||
layoutParams.bottomMargin = targetBottomMargin
|
||||
rvChat.layoutParams = layoutParams
|
||||
}
|
||||
}
|
||||
|
||||
private fun inviteSpeaker(peerId: Long) {
|
||||
agora.sendRawMessageToPeer(
|
||||
receiverUid = peerId.toString(),
|
||||
@@ -1408,7 +1664,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
nickname,
|
||||
rawMessage,
|
||||
can,
|
||||
donationMessage = message
|
||||
donationMessage = message,
|
||||
isSecret = true
|
||||
)
|
||||
)
|
||||
invalidateChat()
|
||||
@@ -1488,6 +1745,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
Logger.e("onJoinChannelSuccess - uid: $uid, channel: $channel")
|
||||
}
|
||||
|
||||
override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
|
||||
super.onStreamMessage(uid, streamId, data)
|
||||
if (!isV2vAvailable || viewModel.isV2vCaptionOn.value != true) {
|
||||
return
|
||||
}
|
||||
|
||||
val rawMessage = data?.toString(Charsets.UTF_8)?.takeIf { it.isNotBlank() } ?: return
|
||||
Logger.d("v2v onStreamMessage: uid=$uid, streamId=$streamId, length=${rawMessage.length}")
|
||||
handleV2vStreamChunk(rawMessage)
|
||||
}
|
||||
|
||||
override fun onActiveSpeaker(uid: Int) {
|
||||
Logger.e("onActiveSpeaker - uid: $uid")
|
||||
super.onActiveSpeaker(uid)
|
||||
@@ -1815,6 +2083,194 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleV2vCaption() {
|
||||
if (!isV2vAvailable || !viewModel.isRoomInfoInitialized()) {
|
||||
return
|
||||
}
|
||||
|
||||
val isOn = viewModel.isV2vCaptionOn.value == true
|
||||
if (isOn) {
|
||||
viewModel.setV2vCaptionEnabled(false)
|
||||
viewModel.stopV2vTranslation()
|
||||
} else {
|
||||
val source = v2vSourceLanguage ?: return
|
||||
val target = v2vTargetLanguage ?: return
|
||||
viewModel.startV2vTranslation(
|
||||
roomInfo = viewModel.roomInfoResponse,
|
||||
sourceLanguage = source,
|
||||
targetLanguage = target,
|
||||
userId = SharedPreferenceManager.userId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateV2vAvailability(response: GetRoomInfoResponse) {
|
||||
val source = mapRoomLanguage(response.creatorLanguageCode)
|
||||
val target = mapDeviceLanguage(LanguageManager.getEffectiveLanguage(this))
|
||||
val available = !source.isNullOrBlank() && !target.isNullOrBlank() && source != target
|
||||
Logger.d(
|
||||
"v2v language mapping - creatorLanguageCode=${response.creatorLanguageCode}, " +
|
||||
"effectiveDeviceLanguage=${LanguageManager.getEffectiveLanguage(this)}, " +
|
||||
"source=$source, target=$target, available=$available"
|
||||
)
|
||||
|
||||
isV2vAvailable = available
|
||||
v2vSourceLanguage = if (available) source else null
|
||||
v2vTargetLanguage = if (available) target else null
|
||||
|
||||
binding.tvV2vSignatureSwitch.visibility = if (available) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
viewModel.setV2vCaptionEnabled(false)
|
||||
binding.tvV2vCaption.text = ""
|
||||
binding.tvV2vCaption.visibility = View.GONE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
viewModel.stopV2vTranslation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapRoomLanguage(languageCode: String?): String? {
|
||||
return when (languageCode?.trim()?.lowercase()) {
|
||||
"ko" -> "ko-KR"
|
||||
"en" -> "en-US"
|
||||
"ja" -> "ja-JP"
|
||||
else -> "ko-KR"
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapDeviceLanguage(languageCode: String): String? {
|
||||
return when (languageCode.lowercase()) {
|
||||
"ko" -> "ko-KR"
|
||||
"en" -> "en-US"
|
||||
"ja" -> "ja-JP"
|
||||
else -> "ja-JP"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vRawMessage(rawMessage: String) {
|
||||
Logger.d("v2v raw message before parse: $rawMessage")
|
||||
|
||||
try {
|
||||
val json = JSONObject(rawMessage)
|
||||
if (json.has("part_idx") && json.has("part_sum") && json.has("content")) {
|
||||
handleV2vChunk(json)
|
||||
return
|
||||
}
|
||||
handleV2vTranslation(json)
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to parse v2v message: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vStreamChunk(rawMessage: String) {
|
||||
val chunk = parseV2vChunk(rawMessage)
|
||||
if (chunk == null) {
|
||||
handleV2vRawMessage(rawMessage)
|
||||
return
|
||||
}
|
||||
|
||||
clearExpiredV2vCache()
|
||||
|
||||
val chunks = v2vMessageCache.getOrPut(chunk.messageId) { mutableListOf() }
|
||||
if (chunks.none { it.partIdx == chunk.partIdx }) {
|
||||
chunks.add(V2vMessageChunk(chunk.partIdx, chunk.partSum, chunk.content))
|
||||
v2vMessageCacheTimestamps[chunk.messageId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
if (chunks.size == chunk.partSum) {
|
||||
val fullContent = chunks.sortedBy { it.partIdx }
|
||||
.joinToString(separator = "") { it.content }
|
||||
v2vMessageCache.remove(chunk.messageId)
|
||||
v2vMessageCacheTimestamps.remove(chunk.messageId)
|
||||
val decoded = String(Base64.decode(fullContent, Base64.DEFAULT))
|
||||
handleV2vRawMessage(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseV2vChunk(rawMessage: String): V2vChunkEnvelope? {
|
||||
val parts = rawMessage.split("|", limit = 4)
|
||||
if (parts.size != 4) {
|
||||
return null
|
||||
}
|
||||
|
||||
val messageId = parts[0]
|
||||
val partIdx = parts[1].toIntOrNull()
|
||||
val partSum = parts[2].toIntOrNull()
|
||||
val content = parts[3]
|
||||
|
||||
if (messageId.isBlank() || partIdx == null || partSum == null || partIdx <= 0 || partSum <= 0 || content.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return V2vChunkEnvelope(
|
||||
messageId = messageId,
|
||||
partIdx = partIdx,
|
||||
partSum = partSum,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleV2vChunk(json: JSONObject) {
|
||||
val messageId = json.optString("message_id")
|
||||
val partIdx = json.optInt("part_idx", -1)
|
||||
val partSum = json.optInt("part_sum", -1)
|
||||
val content = json.optString("content")
|
||||
|
||||
if (messageId.isBlank() || partIdx < 0 || partSum <= 0 || content.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
clearExpiredV2vCache()
|
||||
|
||||
val chunks = v2vMessageCache.getOrPut(messageId) { mutableListOf() }
|
||||
if (chunks.none { it.partIdx == partIdx }) {
|
||||
chunks.add(V2vMessageChunk(partIdx, partSum, content))
|
||||
v2vMessageCacheTimestamps[messageId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
if (chunks.size == partSum) {
|
||||
val fullContent = chunks.sortedBy { it.partIdx }
|
||||
.joinToString(separator = "") { it.content }
|
||||
v2vMessageCache.remove(messageId)
|
||||
v2vMessageCacheTimestamps.remove(messageId)
|
||||
val decoded = String(Base64.decode(fullContent, Base64.DEFAULT))
|
||||
handleV2vRawMessage(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearExpiredV2vCache() {
|
||||
val now = System.currentTimeMillis()
|
||||
val expiredKeys = v2vMessageCacheTimestamps
|
||||
.filter { now - it.value > 10_000 }
|
||||
.keys
|
||||
expiredKeys.forEach {
|
||||
v2vMessageCache.remove(it)
|
||||
v2vMessageCacheTimestamps.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vTranslation(json: JSONObject) {
|
||||
val type = json.optString("object")
|
||||
if (type != "user.translation" && type != "agent.translation") {
|
||||
return
|
||||
}
|
||||
|
||||
val text = json.optString("text")
|
||||
if (text.isBlank() || viewModel.isV2vCaptionOn.value != true) {
|
||||
return
|
||||
}
|
||||
|
||||
handler.post {
|
||||
binding.tvV2vCaption.text = text
|
||||
binding.tvV2vCaption.visibility = View.VISIBLE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAgora() {
|
||||
agora = Agora(
|
||||
uid = SharedPreferenceManager.userId,
|
||||
@@ -3210,5 +3666,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
|
||||
companion object {
|
||||
private const val NO_CHATTING_TIME = 180L
|
||||
var isForeground: Boolean = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,14 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vAdvancedFeatures
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vAsr
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vJoinProperties
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vJoinRequest
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vParameters
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vRepository
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vTranslation
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vTts
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
@@ -45,8 +53,11 @@ class LiveRoomViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val reportRepository: ReportRepository,
|
||||
private val rouletteRepository: RouletteRepository,
|
||||
private val userEventRepository: UserEventRepository
|
||||
private val userEventRepository: UserEventRepository,
|
||||
private val v2vRepository: V2vRepository
|
||||
) : BaseViewModel() {
|
||||
private val v2vPreset = "v2vt_base"
|
||||
|
||||
private val _roomInfoLiveData = MutableLiveData<GetRoomInfoResponse>()
|
||||
val roomInfoLiveData: LiveData<GetRoomInfoResponse>
|
||||
get() = _roomInfoLiveData
|
||||
@@ -99,6 +110,12 @@ class LiveRoomViewModel(
|
||||
val isSignatureOn: LiveData<Boolean>
|
||||
get() = _isSignatureOn
|
||||
|
||||
private var _isV2vCaptionOn = MutableLiveData(false)
|
||||
val isV2vCaptionOn: LiveData<Boolean>
|
||||
get() = _isV2vCaptionOn
|
||||
|
||||
private var v2vAgentId: String? = null
|
||||
|
||||
private val blockedMemberIdList: MutableList<Long> = mutableListOf()
|
||||
|
||||
// 메인 스레드 보장을 위한 Handler (postValue의 병합(coalescing) 이슈 방지 목적)
|
||||
@@ -275,6 +292,81 @@ class LiveRoomViewModel(
|
||||
blockedMemberIdList.add(memberId)
|
||||
}
|
||||
|
||||
fun setV2vCaptionEnabled(isEnabled: Boolean) {
|
||||
_isV2vCaptionOn.value = isEnabled
|
||||
}
|
||||
|
||||
fun startV2vTranslation(
|
||||
roomInfo: GetRoomInfoResponse,
|
||||
sourceLanguage: String,
|
||||
targetLanguage: String,
|
||||
userId: Long
|
||||
) {
|
||||
val agentRtcUid = "${userId}333"
|
||||
val request = V2vJoinRequest(
|
||||
name = "v2v-translation-agent-${System.currentTimeMillis()}",
|
||||
preset = v2vPreset,
|
||||
properties = V2vJoinProperties(
|
||||
channel = roomInfo.channelName,
|
||||
token = roomInfo.v2vWorkerToken,
|
||||
agentRtcUid = agentRtcUid,
|
||||
remoteRtcUids = listOf(roomInfo.creatorId.toString()),
|
||||
idleTimeout = 300,
|
||||
advancedFeatures = V2vAdvancedFeatures(enableRtm = false),
|
||||
parameters = V2vParameters(dataChannel = "datastream"),
|
||||
asr = V2vAsr(language = sourceLanguage),
|
||||
translation = V2vTranslation(language = targetLanguage),
|
||||
tts = V2vTts(enable = false)
|
||||
)
|
||||
)
|
||||
Logger.d(
|
||||
"v2v join request - roomId=${roomInfo.roomId}, creatorId=${roomInfo.creatorId}, " +
|
||||
"sourceLanguage=$sourceLanguage, targetLanguage=$targetLanguage"
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
v2vRepository.join(request)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
v2vAgentId = it.agentId
|
||||
_isV2vCaptionOn.value = true
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_live_room_unknown_error)
|
||||
)
|
||||
_isV2vCaptionOn.value = false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun stopV2vTranslation() {
|
||||
val agentId = v2vAgentId ?: return
|
||||
compositeDisposable.add(
|
||||
v2vRepository.leave(agentId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
v2vAgentId = null
|
||||
_isV2vCaptionOn.value = false
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_live_room_unknown_error)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeBlockedMember(memberId: Long) {
|
||||
blockedMemberIdList.remove(memberId)
|
||||
}
|
||||
@@ -340,12 +432,18 @@ class LiveRoomViewModel(
|
||||
onSuccess(message)
|
||||
}
|
||||
|
||||
fun creatorFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) {
|
||||
fun creatorFollow(
|
||||
creatorId: Long,
|
||||
roomId: Long,
|
||||
notify: Boolean = true,
|
||||
isGetUserProfile: Boolean = false
|
||||
) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.creatorFollow(
|
||||
creatorId,
|
||||
"Bearer ${SharedPreferenceManager.token}"
|
||||
creatorId = creatorId,
|
||||
notify = notify,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.room.create
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomType
|
||||
|
||||
@Keep
|
||||
@@ -11,6 +12,7 @@ data class CreateLiveRoomRequest(
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String? = null,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("genderRestriction") val genderRestriction: GenderRestriction = GenderRestriction.ALL,
|
||||
@SerializedName("tags") val tags: List<String>,
|
||||
@SerializedName("numberOfPeople") val numberOfPeople: Int,
|
||||
@SerializedName("beginDateTimeString") val beginDateTimeString: String? = null,
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.room.create
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
|
||||
@Keep
|
||||
data class GetRecentRoomInfoResponse(
|
||||
@@ -9,5 +10,7 @@ data class GetRecentRoomInfoResponse(
|
||||
@SerializedName("notice") val notice: String,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
||||
@SerializedName("coverImagePath") val coverImagePath: String,
|
||||
@SerializedName("numberOfPeople") val numberOfPeople: Int
|
||||
@SerializedName("numberOfPeople") val numberOfPeople: Int,
|
||||
@SerializedName("genderRestriction")
|
||||
val genderRestriction: GenderRestriction = GenderRestriction.ALL
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomCreateBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemLiveTagSelectedBinding
|
||||
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomType
|
||||
import kr.co.vividnext.sodalive.live.room.tag.LiveTagFragment
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
@@ -592,6 +593,18 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
|
||||
viewModel.setAdult(true)
|
||||
}
|
||||
|
||||
binding.llGenderAll.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.ALL)
|
||||
}
|
||||
|
||||
binding.llGenderMaleOnly.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.MALE_ONLY)
|
||||
}
|
||||
|
||||
binding.llGenderFemaleOnly.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.FEMALE_ONLY)
|
||||
}
|
||||
|
||||
viewModel.isAdultLiveData.observe(this) {
|
||||
if (it) {
|
||||
binding.ivAgeAll.visibility = View.GONE
|
||||
@@ -630,6 +643,16 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.llGenderRestriction.visibility = if (it) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.genderRestrictionLiveData.observe(this) { restriction ->
|
||||
updateGenderRestrictionSelection(restriction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,4 +873,51 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGenderRestrictionSelection(restriction: GenderRestriction) {
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.ALL,
|
||||
imageView = binding.ivGenderAll,
|
||||
container = binding.llGenderAll,
|
||||
textView = binding.tvGenderAll
|
||||
)
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.MALE_ONLY,
|
||||
imageView = binding.ivGenderMaleOnly,
|
||||
container = binding.llGenderMaleOnly,
|
||||
textView = binding.tvGenderMaleOnly
|
||||
)
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.FEMALE_ONLY,
|
||||
imageView = binding.ivGenderFemaleOnly,
|
||||
container = binding.llGenderFemaleOnly,
|
||||
textView = binding.tvGenderFemaleOnly
|
||||
)
|
||||
}
|
||||
|
||||
private fun setGenderRestrictionSelected(
|
||||
isSelected: Boolean,
|
||||
imageView: ImageView,
|
||||
container: LinearLayout,
|
||||
textView: TextView
|
||||
) {
|
||||
imageView.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
container.setBackgroundResource(
|
||||
if (isSelected) {
|
||||
R.drawable.bg_round_corner_6_7_3bb9f1
|
||||
} else {
|
||||
R.drawable.bg_round_corner_6_7_13181b
|
||||
}
|
||||
)
|
||||
textView.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
if (isSelected) {
|
||||
R.color.color_eeeeee
|
||||
} else {
|
||||
R.color.color_3bb9f1
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.common.UiText
|
||||
import kr.co.vividnext.sodalive.common.UiText.DynamicString
|
||||
import kr.co.vividnext.sodalive.common.UiText.StringResource
|
||||
import kr.co.vividnext.sodalive.live.LiveRepository
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomType
|
||||
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
|
||||
import kr.co.vividnext.sodalive.R
|
||||
@@ -27,6 +28,10 @@ class LiveRoomCreateViewModel(
|
||||
private val repository: LiveRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val MINIMUM_PAID_PRICE = 30
|
||||
}
|
||||
|
||||
enum class SelectedMenu {
|
||||
MENU_1, MENU_2, MENU_3
|
||||
}
|
||||
@@ -67,6 +72,10 @@ class LiveRoomCreateViewModel(
|
||||
val isAdultLiveData: LiveData<Boolean>
|
||||
get() = _isAdultLiveData
|
||||
|
||||
private val _genderRestrictionLiveData = MutableLiveData(GenderRestriction.ALL)
|
||||
val genderRestrictionLiveData: LiveData<GenderRestriction>
|
||||
get() = _genderRestrictionLiveData
|
||||
|
||||
private val _selectedMenuLiveData = MutableLiveData<SelectedMenu>()
|
||||
val selectedMenuLiveData: LiveData<SelectedMenu>
|
||||
get() = _selectedMenuLiveData
|
||||
@@ -118,12 +127,18 @@ class LiveRoomCreateViewModel(
|
||||
fun createLiveRoom(onSuccess: (CreateLiveRoomResponse) -> Unit) {
|
||||
if (!_isLoading.value!! && validateData()) {
|
||||
_isLoading.postValue(true)
|
||||
val normalizedGenderRestriction = if (_isAdultLiveData.value == true) {
|
||||
_genderRestrictionLiveData.value!!
|
||||
} else {
|
||||
GenderRestriction.ALL
|
||||
}
|
||||
val request = CreateLiveRoomRequest(
|
||||
title = title,
|
||||
price = _priceLiveData.value!!,
|
||||
content = content,
|
||||
coverImageUrl = coverImagePath,
|
||||
isAdult = _isAdultLiveData.value!!,
|
||||
genderRestriction = normalizedGenderRestriction,
|
||||
tags = tags.toList(),
|
||||
numberOfPeople = numberOfPeople,
|
||||
beginDateTimeString = if (
|
||||
@@ -253,15 +268,28 @@ class LiveRoomCreateViewModel(
|
||||
return false
|
||||
}
|
||||
|
||||
val price = _priceLiveData.value ?: 0
|
||||
if (price in 1 until MINIMUM_PAID_PRICE) {
|
||||
_toastLiveData.postValue(StringResource(R.string.msg_live_room_create_minimum_paid_price))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun setPrice(price: Int) {
|
||||
_priceLiveData.value = price
|
||||
_priceLiveData.value = price.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
fun setAdult(isAdult: Boolean) {
|
||||
_isAdultLiveData.value = isAdult
|
||||
if (!isAdult) {
|
||||
_genderRestrictionLiveData.value = GenderRestriction.ALL
|
||||
}
|
||||
}
|
||||
|
||||
fun setGenderRestriction(genderRestriction: GenderRestriction) {
|
||||
_genderRestrictionLiveData.value = genderRestriction
|
||||
}
|
||||
|
||||
fun setAvailableJoinCreator(isAvailableJoinCreator: Boolean) {
|
||||
@@ -279,6 +307,7 @@ class LiveRoomCreateViewModel(
|
||||
if (it.success && it.data != null) {
|
||||
coverImageFile = null
|
||||
coverImagePath = it.data.coverImagePath
|
||||
_genderRestrictionLiveData.value = it.data.genderRestriction
|
||||
onSuccess(it.data!!)
|
||||
|
||||
_toastLiveData.postValue(
|
||||
|
||||
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.live.room.detail
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -14,6 +15,8 @@ data class GetRoomDetailResponse(
|
||||
@SerializedName("notice") val notice: String,
|
||||
@SerializedName("isPaid") val isPaid: Boolean,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("genderRestriction")
|
||||
val genderRestriction: GenderRestriction = GenderRestriction.ALL,
|
||||
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
||||
@SerializedName("password") val password: Int?,
|
||||
@SerializedName("tags") val tags: List<String>,
|
||||
@@ -34,8 +37,9 @@ data class GetRoomDetailManager(
|
||||
@SerializedName("introduce") val introduce: String,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String?,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String?,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String?,
|
||||
@SerializedName("blogUrl") val blogUrl: String?,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String?,
|
||||
@SerializedName("xUrl") val xUrl: String?,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String,
|
||||
@SerializedName("isCreator") val isCreator: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class LiveRoomDetailFragment(
|
||||
private val roomId: Long,
|
||||
@@ -122,13 +123,14 @@ class LiveRoomDetailFragment(
|
||||
val locale = Locale(LanguageManager.getEffectiveLanguage(requireContext()))
|
||||
val wrappedContext = LocaleHelper.wrap(requireContext())
|
||||
|
||||
binding.tv19.visibility = if (response.isAdult) {
|
||||
binding.tvTitle.text = response.title
|
||||
|
||||
binding.ivShield.visibility = if (response.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.tvTitle.text = response.title
|
||||
binding.tvDate.text = response.beginDateTimeUtc.convertDateFormat(
|
||||
from = "yyyy-MM-dd'T'HH:mm:ss",
|
||||
to = wrappedContext.getString(R.string.screen_live_room_detail_date_format),
|
||||
@@ -272,26 +274,14 @@ class LiveRoomDetailFragment(
|
||||
}
|
||||
|
||||
if (
|
||||
manager.websiteUrl.isNullOrBlank() ||
|
||||
!URLUtil.isValidUrl(manager.websiteUrl)
|
||||
manager.kakaoOpenChatUrl.isNullOrBlank() ||
|
||||
!URLUtil.isValidUrl(manager.kakaoOpenChatUrl)
|
||||
) {
|
||||
binding.ivManagerWebsite.visibility = View.GONE
|
||||
binding.ivManagerOpenChat.visibility = View.GONE
|
||||
} else {
|
||||
binding.ivManagerWebsite.visibility = View.VISIBLE
|
||||
binding.ivManagerWebsite.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.websiteUrl)))
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
manager.blogUrl.isNullOrBlank() ||
|
||||
!URLUtil.isValidUrl(manager.blogUrl)
|
||||
) {
|
||||
binding.ivManagerBlog.visibility = View.GONE
|
||||
} else {
|
||||
binding.ivManagerBlog.visibility = View.VISIBLE
|
||||
binding.ivManagerBlog.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.blogUrl)))
|
||||
binding.ivManagerOpenChat.visibility = View.VISIBLE
|
||||
binding.ivManagerOpenChat.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, manager.kakaoOpenChatUrl.toUri()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +293,7 @@ class LiveRoomDetailFragment(
|
||||
} else {
|
||||
binding.ivManagerInstagram.visibility = View.VISIBLE
|
||||
binding.ivManagerInstagram.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.instagramUrl)))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, manager.instagramUrl.toUri()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +305,7 @@ class LiveRoomDetailFragment(
|
||||
} else {
|
||||
binding.ivManagerYoutube.visibility = View.VISIBLE
|
||||
binding.ivManagerYoutube.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.youtubeUrl)))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, manager.youtubeUrl.toUri()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
@@ -26,6 +27,9 @@ class LiveRoomDonationDialog(
|
||||
private val activity: AppCompatActivity,
|
||||
layoutInflater: LayoutInflater,
|
||||
isLiveDonation: Boolean = false,
|
||||
messageMaxLength: Int = 1000,
|
||||
secretToggleLabelResId: Int = R.string.screen_live_room_secret_mission,
|
||||
applySecretMissionMessageHint: Boolean = true,
|
||||
onClickDonation: (Int, String, Boolean) -> Unit
|
||||
) {
|
||||
|
||||
@@ -47,11 +51,17 @@ class LiveRoomDonationDialog(
|
||||
bottomSheetBehavior.skipCollapsed = true
|
||||
}
|
||||
|
||||
dialogView.etDonationMessage.filters = arrayOf(InputFilter.LengthFilter(messageMaxLength))
|
||||
dialogView.etDonationMessage.hint = activity.getString(
|
||||
R.string.screen_live_room_donation_message_hint_format,
|
||||
messageMaxLength
|
||||
)
|
||||
|
||||
dialogView.tvCancel.setOnClickListener { bottomSheetDialog.dismiss() }
|
||||
dialogView.tvDonation.setOnClickListener {
|
||||
try {
|
||||
val can = dialogView.etDonationCan.text.toString().toInt()
|
||||
val message = dialogView.etDonationMessage.text.toString().prefix(1000)
|
||||
val message = dialogView.etDonationMessage.text.toString().prefix(messageMaxLength)
|
||||
|
||||
if (isLiveDonation) {
|
||||
val isSecret = dialogView.tvSecret.isSelected
|
||||
@@ -95,13 +105,20 @@ class LiveRoomDonationDialog(
|
||||
|
||||
if (isLiveDonation) {
|
||||
dialogView.rlSecret.visibility = View.VISIBLE
|
||||
dialogView.tvSecret.text = activity.getString(secretToggleLabelResId)
|
||||
dialogView.tvSecret.setOnClickListener {
|
||||
val isSelected = dialogView.tvSecret.isSelected
|
||||
dialogView.tvSecret.isSelected = !isSelected
|
||||
dialogView.etDonationMessage.hint = if (!isSelected) {
|
||||
activity.getString(R.string.screen_live_room_secret_mission_hint)
|
||||
dialogView.etDonationMessage.hint = if (!isSelected && applySecretMissionMessageHint) {
|
||||
activity.getString(
|
||||
R.string.screen_live_room_secret_mission_hint_format,
|
||||
messageMaxLength
|
||||
)
|
||||
} else {
|
||||
activity.getString(R.string.screen_live_room_donation_message_hint)
|
||||
activity.getString(
|
||||
R.string.screen_live_room_donation_message_hint_format,
|
||||
messageMaxLength
|
||||
)
|
||||
}
|
||||
dialogView.etDonationCan.hint = if (!isSelected) {
|
||||
activity.getString(R.string.screen_live_room_secret_mission_input_min)
|
||||
|
||||
@@ -12,6 +12,7 @@ data class GetRoomInfoResponse(
|
||||
@SerializedName("channelName") val channelName: String,
|
||||
@SerializedName("rtcToken") val rtcToken: String,
|
||||
@SerializedName("rtmToken") val rtmToken: String,
|
||||
@SerializedName("v2vWorkerToken") val v2vWorkerToken: String,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String,
|
||||
@@ -24,6 +25,7 @@ data class GetRoomInfoResponse(
|
||||
@SerializedName("managerList") val managerList: List<LiveRoomMember>,
|
||||
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
|
||||
@SerializedName("menuPan") val menuPan: String,
|
||||
@SerializedName("creatorLanguageCode") val creatorLanguageCode: String?,
|
||||
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
|
||||
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
||||
@SerializedName("password") val password: String? = null
|
||||
|
||||
@@ -11,8 +11,9 @@ data class GetLiveRoomUserProfileResponse(
|
||||
@SerializedName("gender") val gender: String,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String,
|
||||
@SerializedName("blogUrl") val blogUrl: String,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String?,
|
||||
@SerializedName("xUrl") val xUrl: String?,
|
||||
@SerializedName("introduce") val introduce: String,
|
||||
@SerializedName("tags") val tags: String,
|
||||
@SerializedName("isSpeaker") val isSpeaker: Boolean?,
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.room.update
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
|
||||
@Keep
|
||||
data class EditLiveRoomInfoRequest(
|
||||
@@ -13,5 +14,6 @@ data class EditLiveRoomInfoRequest(
|
||||
@SerializedName("menuPanId") val menuPanId: Long = 0,
|
||||
@SerializedName("menuPan") val menuPan: String = "",
|
||||
@SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean? = null,
|
||||
@SerializedName("isAdult") val isAdult: Boolean? = null
|
||||
@SerializedName("isAdult") val isAdult: Boolean? = null,
|
||||
@SerializedName("genderRestriction") val genderRestriction: GenderRestriction? = null
|
||||
)
|
||||
|
||||
@@ -7,8 +7,10 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.jakewharton.rxbinding4.widget.textChanges
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
@@ -16,8 +18,10 @@ import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomEditBinding
|
||||
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -170,6 +174,32 @@ class LiveRoomEditActivity : BaseActivity<ActivityLiveRoomEditBinding>(
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
if (SharedPreferenceManager.isAuth) {
|
||||
binding.llSetAdult.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.llSetAdult.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.llAgeAll.setOnClickListener {
|
||||
viewModel.setAdult(false)
|
||||
}
|
||||
|
||||
binding.llAge19.setOnClickListener {
|
||||
viewModel.setAdult(true)
|
||||
}
|
||||
|
||||
binding.llGenderAll.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.ALL)
|
||||
}
|
||||
|
||||
binding.llGenderMaleOnly.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.MALE_ONLY)
|
||||
}
|
||||
|
||||
binding.llGenderFemaleOnly.setOnClickListener {
|
||||
viewModel.setGenderRestriction(GenderRestriction.FEMALE_ONLY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
@@ -233,5 +263,102 @@ class LiveRoomEditActivity : BaseActivity<ActivityLiveRoomEditBinding>(
|
||||
viewModel.reservationTimeLiveData.observe(this) {
|
||||
binding.tvReservationTime.text = it
|
||||
}
|
||||
|
||||
viewModel.isAdultLiveData.observe(this) {
|
||||
if (it) {
|
||||
binding.ivAgeAll.visibility = View.GONE
|
||||
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
|
||||
binding.tvAgeAll.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
|
||||
binding.ivAge19.visibility = View.VISIBLE
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
binding.tvAge19.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
} else {
|
||||
binding.ivAge19.visibility = View.GONE
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
|
||||
binding.tvAge19.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
|
||||
binding.ivAgeAll.visibility = View.VISIBLE
|
||||
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
binding.tvAgeAll.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.llGenderRestriction.visibility = if (it) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.genderRestrictionLiveData.observe(this) { restriction ->
|
||||
updateGenderRestrictionSelection(restriction)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGenderRestrictionSelection(restriction: GenderRestriction) {
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.ALL,
|
||||
imageView = binding.ivGenderAll,
|
||||
container = binding.llGenderAll,
|
||||
textView = binding.tvGenderAll
|
||||
)
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.MALE_ONLY,
|
||||
imageView = binding.ivGenderMaleOnly,
|
||||
container = binding.llGenderMaleOnly,
|
||||
textView = binding.tvGenderMaleOnly
|
||||
)
|
||||
setGenderRestrictionSelected(
|
||||
isSelected = restriction == GenderRestriction.FEMALE_ONLY,
|
||||
imageView = binding.ivGenderFemaleOnly,
|
||||
container = binding.llGenderFemaleOnly,
|
||||
textView = binding.tvGenderFemaleOnly
|
||||
)
|
||||
}
|
||||
|
||||
private fun setGenderRestrictionSelected(
|
||||
isSelected: Boolean,
|
||||
imageView: android.widget.ImageView,
|
||||
container: android.widget.LinearLayout,
|
||||
textView: android.widget.TextView
|
||||
) {
|
||||
imageView.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
container.setBackgroundResource(
|
||||
if (isSelected) {
|
||||
R.drawable.bg_round_corner_6_7_3bb9f1
|
||||
} else {
|
||||
R.drawable.bg_round_corner_6_7_13181b
|
||||
}
|
||||
)
|
||||
textView.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
if (isSelected) {
|
||||
R.color.color_eeeeee
|
||||
} else {
|
||||
R.color.color_3bb9f1
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@ import com.google.gson.Gson
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.UiText
|
||||
import kr.co.vividnext.sodalive.common.UiText.DynamicString
|
||||
import kr.co.vividnext.sodalive.common.UiText.StringResource
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
||||
import kr.co.vividnext.sodalive.live.LiveRepository
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.Locale
|
||||
@@ -39,6 +39,14 @@ class LiveRoomEditViewModel(
|
||||
val toastLiveData: LiveData<UiText?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _isAdultLiveData = MutableLiveData(false)
|
||||
val isAdultLiveData: LiveData<Boolean>
|
||||
get() = _isAdultLiveData
|
||||
|
||||
private val _genderRestrictionLiveData = MutableLiveData(GenderRestriction.ALL)
|
||||
val genderRestrictionLiveData: LiveData<GenderRestriction>
|
||||
get() = _genderRestrictionLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
@@ -51,6 +59,8 @@ class LiveRoomEditViewModel(
|
||||
var beginDate = ""
|
||||
var beginTime = ""
|
||||
var beginDateTimeStr = ""
|
||||
var isAdultValue = false
|
||||
var genderRestrictionValue = GenderRestriction.ALL
|
||||
|
||||
fun setReservationDate(dateString: String) {
|
||||
_reservationDateLiveData.postValue(dateString)
|
||||
@@ -63,6 +73,11 @@ class LiveRoomEditViewModel(
|
||||
fun updateLiveRoom(onSuccess: () -> Unit) {
|
||||
if (!_isLoading.value!! && validateData()) {
|
||||
_isLoading.value = true
|
||||
val normalizedGenderRestriction = if (isAdultValue) {
|
||||
genderRestrictionValue
|
||||
} else {
|
||||
GenderRestriction.ALL
|
||||
}
|
||||
val request = EditLiveRoomInfoRequest(
|
||||
title = if (title != roomDetail.title) {
|
||||
title
|
||||
@@ -84,14 +99,28 @@ class LiveRoomEditViewModel(
|
||||
} else {
|
||||
null
|
||||
},
|
||||
timezone = TimeZone.getDefault().id
|
||||
timezone = TimeZone.getDefault().id,
|
||||
isAdult = if (isAdultValue != roomDetail.isAdult) {
|
||||
isAdultValue
|
||||
} else {
|
||||
null
|
||||
},
|
||||
genderRestriction = if (!isAdultValue) {
|
||||
GenderRestriction.ALL
|
||||
} else if (normalizedGenderRestriction != roomDetail.genderRestriction) {
|
||||
normalizedGenderRestriction
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
request.title == null &&
|
||||
request.notice == null &&
|
||||
request.numberOfPeople == null &&
|
||||
request.beginDateTimeString == null
|
||||
request.beginDateTimeString == null &&
|
||||
request.isAdult == null &&
|
||||
request.genderRestriction == null
|
||||
) {
|
||||
_toastLiveData.value = StringResource(R.string.msg_live_room_edit_no_changes)
|
||||
_isLoading.value = false
|
||||
@@ -170,6 +199,16 @@ class LiveRoomEditViewModel(
|
||||
)
|
||||
|
||||
beginDateTimeStr = "$beginDate $beginTime"
|
||||
|
||||
isAdultValue = roomDetail.isAdult
|
||||
genderRestrictionValue = if (roomDetail.isAdult) {
|
||||
roomDetail.genderRestriction
|
||||
} else {
|
||||
GenderRestriction.ALL
|
||||
}
|
||||
|
||||
_isAdultLiveData.value = isAdultValue
|
||||
_genderRestrictionLiveData.value = genderRestrictionValue
|
||||
}
|
||||
|
||||
private fun validateData(): Boolean {
|
||||
@@ -196,4 +235,17 @@ class LiveRoomEditViewModel(
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun setAdult(isAdult: Boolean) {
|
||||
isAdultValue = isAdult
|
||||
_isAdultLiveData.value = isAdult
|
||||
if (!isAdult) {
|
||||
setGenderRestriction(GenderRestriction.ALL)
|
||||
}
|
||||
}
|
||||
|
||||
fun setGenderRestriction(genderRestriction: GenderRestriction) {
|
||||
genderRestrictionValue = genderRestriction
|
||||
_genderRestrictionLiveData.value = genderRestriction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,29 @@ package kr.co.vividnext.sodalive.main
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.app.SodaLiveApp
|
||||
import kr.co.vividnext.sodalive.audition.AuditionActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
||||
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import java.util.Locale
|
||||
|
||||
class DeepLinkActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val data: Uri? = intent?.data
|
||||
val deepLinkExtras = buildDeepLinkExtras(intent)
|
||||
val deepLinkUrl = deepLinkExtras?.getString("deep_link")
|
||||
|
||||
if (data != null && data.scheme != null) {
|
||||
val host = data.host
|
||||
val path = data.path
|
||||
@@ -30,15 +42,433 @@ class DeepLinkActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// 그 외 일반 딥링크는 기존처럼 Splash로 위임
|
||||
if (SodaLiveApp.isAppInForeground && LiveRoomActivity.isForeground && deepLinkExtras != null) {
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(
|
||||
Intent(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM).apply {
|
||||
putExtra(Constants.EXTRA_DATA, deepLinkExtras)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (SodaLiveApp.isAppInForeground) {
|
||||
if (!deepLinkUrl.isNullOrBlank()) {
|
||||
deepLinkExtras?.let {
|
||||
if (routeForegroundDeepLink(it)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
||||
}
|
||||
)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, SplashActivity::class.java).apply {
|
||||
setData(intent.data)
|
||||
setData(data)
|
||||
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
||||
}
|
||||
)
|
||||
finish()
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 1000)
|
||||
private fun buildDeepLinkExtras(intent: Intent): Bundle? {
|
||||
val extras = Bundle()
|
||||
|
||||
val data = intent.data
|
||||
|
||||
data?.toString()?.takeIf { it.isNotBlank() }?.let {
|
||||
extras.putString("deep_link", it)
|
||||
}
|
||||
|
||||
fun putIfAbsent(key: String, value: String?) {
|
||||
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
|
||||
extras.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
fun putQuery(key: String) {
|
||||
val value = data.getQueryParameter(key)
|
||||
if (!value.isNullOrBlank()) {
|
||||
extras.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
putQuery("room_id")
|
||||
putQuery("channel_id")
|
||||
putQuery("message_id")
|
||||
putQuery("audition_id")
|
||||
putQuery("content_id")
|
||||
putQuery("deep_link_value")
|
||||
putQuery("deep_link_sub5")
|
||||
putQuery("postId")
|
||||
putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID)
|
||||
putQuery(Constants.EXTRA_COMMUNITY_POST_ID)
|
||||
}
|
||||
|
||||
extras.getString("postId")?.takeIf { it.isNotBlank() }?.let {
|
||||
if (!extras.containsKey(Constants.EXTRA_COMMUNITY_POST_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
intent.getBundleExtra(Constants.EXTRA_DATA)?.let { source ->
|
||||
fun copyString(key: String) {
|
||||
val value = source.getString(key)
|
||||
if (!value.isNullOrBlank()) {
|
||||
extras.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
copyString("room_id")
|
||||
copyString("channel_id")
|
||||
copyString("message_id")
|
||||
copyString("audition_id")
|
||||
copyString("content_id")
|
||||
copyString("deep_link")
|
||||
copyString("deep_link_value")
|
||||
copyString("deep_link_sub5")
|
||||
copyString(Constants.EXTRA_COMMUNITY_CREATOR_ID)
|
||||
copyString(Constants.EXTRA_COMMUNITY_POST_ID)
|
||||
|
||||
source.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString("room_id", it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString("channel_id", it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString("message_id", it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString("audition_id", it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString("content_id", it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it.toString())
|
||||
}
|
||||
source.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }?.let {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString("room_id", it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_USER_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString("channel_id", it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_MESSAGE_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString("message_id", it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_AUDITION_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString("audition_id", it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString("content_id", it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it.toString())
|
||||
}
|
||||
intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0).takeIf { it > 0 }?.let {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it.toString())
|
||||
}
|
||||
|
||||
intent.getStringExtra("deep_link")?.takeIf { it.isNotBlank() }?.let {
|
||||
extras.putString("deep_link", it)
|
||||
}
|
||||
|
||||
intent.getStringExtra("deepLink")?.takeIf { it.isNotBlank() }?.let {
|
||||
extras.putString("deep_link", it)
|
||||
}
|
||||
|
||||
intent.getStringExtra("postId")?.takeIf { it.isNotBlank() }?.let {
|
||||
if (!extras.containsKey(Constants.EXTRA_COMMUNITY_POST_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
intent.getStringExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.takeIf { it.isNotBlank() }?.let {
|
||||
if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
applyPathDeepLink(data = data, putIfAbsent = ::putIfAbsent)
|
||||
}
|
||||
|
||||
val deepLinkValue = extras.getString("deep_link_value")
|
||||
val deepLinkValueId = extras.getString("deep_link_sub5")
|
||||
|
||||
if (!deepLinkValue.isNullOrBlank() && !deepLinkValueId.isNullOrBlank()) {
|
||||
when (deepLinkValue.lowercase()) {
|
||||
"live" -> if (!extras.containsKey("room_id")) {
|
||||
extras.putString("room_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"channel" -> if (!extras.containsKey("channel_id")) {
|
||||
extras.putString("channel_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"content" -> if (!extras.containsKey("content_id")) {
|
||||
extras.putString("content_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"audition" -> if (!extras.containsKey("audition_id")) {
|
||||
extras.putString("audition_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"community" -> if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||
}
|
||||
|
||||
"message" -> if (!extras.containsKey("message_id")) {
|
||||
extras.putString("message_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return if (extras.isEmpty) {
|
||||
null
|
||||
} else {
|
||||
extras
|
||||
}
|
||||
}
|
||||
|
||||
private fun routeForegroundDeepLink(bundle: Bundle): Boolean {
|
||||
val roomId = bundle.getString("room_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
|
||||
val channelId = bundle.getString("channel_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
|
||||
val messageId = bundle.getString("message_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
|
||||
val contentId = bundle.getString("content_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
|
||||
val auditionId = bundle.getString("audition_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }
|
||||
val communityCreatorId = bundle.getString(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }
|
||||
val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }
|
||||
|
||||
when {
|
||||
roomId != null && roomId > 0 -> {
|
||||
routeLiveInMain(roomId)
|
||||
return true
|
||||
}
|
||||
|
||||
channelId != null && channelId > 0 -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, channelId)
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
contentId != null && contentId > 0 -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
messageId != null && messageId > 0 -> {
|
||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||
return true
|
||||
}
|
||||
|
||||
communityCreatorId != null && communityCreatorId > 0 -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId)
|
||||
if (communityPostId != null && communityPostId > 0) {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId)
|
||||
}
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
auditionId != null && auditionId > 0 -> {
|
||||
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val deepLinkValue = bundle.getString("deep_link_value")
|
||||
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
|
||||
return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
|
||||
}
|
||||
|
||||
private fun routeByDeepLinkValue(deepLinkValue: String?, deepLinkValueId: Long?): Boolean {
|
||||
if (deepLinkValue.isNullOrBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (deepLinkValue.lowercase(Locale.ROOT)) {
|
||||
"series" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"channel" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"live" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
routeLiveInMain(deepLinkValueId)
|
||||
true
|
||||
}
|
||||
|
||||
"community" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"message" -> {
|
||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
"audition" -> {
|
||||
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPathDeepLink(
|
||||
data: Uri,
|
||||
putIfAbsent: (key: String, value: String?) -> Unit
|
||||
) {
|
||||
val host = data.host?.lowercase(Locale.ROOT).orEmpty()
|
||||
val pathSegments = data.pathSegments.filter { it.isNotBlank() }
|
||||
|
||||
val pathType: String
|
||||
val pathId: String?
|
||||
|
||||
if (host.isNotBlank() && host != "payverse") {
|
||||
pathType = host
|
||||
pathId = pathSegments.firstOrNull()
|
||||
} else if (pathSegments.isNotEmpty()) {
|
||||
pathType = pathSegments[0].lowercase(Locale.ROOT)
|
||||
pathId = pathSegments.getOrNull(1)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
when (pathType) {
|
||||
"live" -> {
|
||||
putIfAbsent("room_id", pathId)
|
||||
putIfAbsent("deep_link_value", "live")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
putIfAbsent("content_id", pathId)
|
||||
putIfAbsent("deep_link_value", "content")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"series" -> {
|
||||
putIfAbsent("deep_link_value", "series")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"community" -> {
|
||||
putIfAbsent("deep_link_value", "community")
|
||||
putIfAbsent(Constants.EXTRA_COMMUNITY_CREATOR_ID, pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"message" -> {
|
||||
putIfAbsent("deep_link_value", "message")
|
||||
putIfAbsent("message_id", pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"audition" -> {
|
||||
putIfAbsent("deep_link_value", "audition")
|
||||
putIfAbsent("audition_id", pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun routeLiveInMain(roomId: Long) {
|
||||
val extras = Bundle().apply {
|
||||
putString("room_id", roomId.toString())
|
||||
putString("deep_link_value", "live")
|
||||
putString("deep_link_sub5", roomId.toString())
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -16,6 +16,9 @@ import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
@@ -24,6 +27,7 @@ import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.gun0912.tedpermission.PermissionListener
|
||||
import com.gun0912.tedpermission.normal.TedPermission
|
||||
@@ -34,6 +38,7 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.audition.AuditionActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.chat.ChatFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
@@ -42,6 +47,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityMainBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeFragment
|
||||
import kr.co.vividnext.sodalive.live.LiveFragment
|
||||
@@ -51,6 +57,11 @@ import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UnstableApi
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
|
||||
@@ -62,25 +73,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
|
||||
|
||||
private var mediaController: MediaController? = null
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||
private val audioContentReceiver = AudioContentReceiver()
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
handler.postDelayed(
|
||||
{
|
||||
initAndVisibleMiniPlayer()
|
||||
},
|
||||
1500
|
||||
)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var playerStateJob: Job? = null
|
||||
|
||||
private fun initAndVisibleMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
@@ -96,32 +93,48 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
if (mediaController != null || mediaControllerFuture != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(applicationContext, componentName)
|
||||
val mediaControllerFuture =
|
||||
val controllerFuture =
|
||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
mediaControllerFuture = controllerFuture
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
try {
|
||||
if (mediaController != null) {
|
||||
controllerFuture.get().release()
|
||||
return@addListener
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
mediaController = controllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setImageResource(
|
||||
if (mediaController?.isPlaying == true) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
it.play()
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Logger.e(throwable, "Failed to connect player service")
|
||||
} finally {
|
||||
mediaControllerFuture = null
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(applicationContext)
|
||||
@@ -165,7 +178,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
}
|
||||
|
||||
private fun deInitMiniPlayer() {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
mediaControllerFuture?.cancel(true)
|
||||
mediaControllerFuture = null
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
}
|
||||
@@ -201,13 +217,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
updatePidAndGaid()
|
||||
getEventPopup()
|
||||
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(
|
||||
preferenceChangeListener
|
||||
)
|
||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
initAndVisibleMiniPlayer()
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
playerStateJob = lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||
if (isRunning) {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||
@@ -216,7 +236,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
playerStateJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -267,105 +287,343 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
}
|
||||
|
||||
private fun executeDeeplink(intent: Intent) {
|
||||
val isLoggedIn =
|
||||
SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
|
||||
if (!isLoggedIn) {
|
||||
executeOneLink()
|
||||
return
|
||||
}
|
||||
|
||||
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
if (
|
||||
SharedPreferenceManager.token.isNotBlank() &&
|
||||
SharedPreferenceManager.token.length > 10 &&
|
||||
bundle != null
|
||||
) {
|
||||
val isHandledFromBundle = if (bundle != null) {
|
||||
try {
|
||||
val roomId = bundle.getString("room_id")?.toLong()
|
||||
?: bundle.getLong(Constants.EXTRA_ROOM_ID)
|
||||
val channelId = bundle.getString("channel_id")?.toLong()
|
||||
?: bundle.getLong(Constants.EXTRA_USER_ID)
|
||||
val messageId = bundle.getString("message_id")?.toLong()
|
||||
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID)
|
||||
val auditionId = bundle.getString("audition_id")?.toLong()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDITION_ID)
|
||||
val contentId = bundle.getString("content_id")?.toLong()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
|
||||
val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
|
||||
|
||||
if (roomId > 0) {
|
||||
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
||||
|
||||
handler.postDelayed({
|
||||
if (isLiveReservation) {
|
||||
liveFragment.reservationRoom(roomId)
|
||||
} else {
|
||||
liveFragment.enterLiveRoom(roomId)
|
||||
}
|
||||
}, 500)
|
||||
} else if (channelId > 0) {
|
||||
val nextIntent = Intent(applicationContext, UserProfileActivity::class.java)
|
||||
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
|
||||
startActivity(nextIntent)
|
||||
} else if (contentId > 0) {
|
||||
val nextIntent = Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
)
|
||||
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
startActivity(nextIntent)
|
||||
} else if (messageId > 0) {
|
||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||
} else if (auditionId > 0) {
|
||||
}
|
||||
executeBundleDeeplink(bundle)
|
||||
} catch (_: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (isHandledFromBundle) {
|
||||
clearDeferredDeepLink()
|
||||
return
|
||||
}
|
||||
|
||||
executeOneLink()
|
||||
}
|
||||
|
||||
private fun executeBundleDeeplink(bundle: Bundle): Boolean {
|
||||
val deepLinkUrl = bundle.getString("deep_link")
|
||||
if (!deepLinkUrl.isNullOrBlank()) {
|
||||
val deepLinkBundle = buildBundleFromDeepLinkUrl(deepLinkUrl)
|
||||
return executeBundleRoute(deepLinkBundle ?: bundle)
|
||||
}
|
||||
|
||||
return executeBundleRoute(bundle)
|
||||
}
|
||||
|
||||
private fun executeBundleRoute(bundle: Bundle): Boolean {
|
||||
val roomId = bundle.getString("room_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
|
||||
val channelId = bundle.getString("channel_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
|
||||
val messageId = bundle.getString("message_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
|
||||
val contentId = bundle.getString("content_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
|
||||
val auditionId = bundle.getString("audition_id")?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }
|
||||
val communityCreatorId = bundle.getString(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }
|
||||
val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull()
|
||||
?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }
|
||||
when {
|
||||
roomId != null && roomId > 0 -> {
|
||||
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
||||
|
||||
handler.postDelayed({
|
||||
liveFragment.enterLiveRoom(roomId)
|
||||
}, 500)
|
||||
return true
|
||||
}
|
||||
|
||||
channelId != null && channelId > 0 -> {
|
||||
val nextIntent = Intent(applicationContext, UserProfileActivity::class.java)
|
||||
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
|
||||
startActivity(nextIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
contentId != null && contentId > 0 -> {
|
||||
val nextIntent = Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
)
|
||||
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
startActivity(nextIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
messageId != null && messageId > 0 -> {
|
||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||
return true
|
||||
}
|
||||
|
||||
communityCreatorId != null && communityCreatorId > 0 -> {
|
||||
val nextIntent = Intent(applicationContext, CreatorCommunityAllActivity::class.java)
|
||||
nextIntent.putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId)
|
||||
if (communityPostId != null && communityPostId > 0) {
|
||||
nextIntent.putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId)
|
||||
}
|
||||
startActivity(nextIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
auditionId != null && auditionId > 0 -> {
|
||||
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val deepLinkValue = bundle.getString("deep_link_value")
|
||||
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
|
||||
|
||||
if (!deepLinkValue.isNullOrBlank()) {
|
||||
return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? {
|
||||
val data = runCatching { deepLinkUrl.toUri() }.getOrNull() ?: return null
|
||||
val extras = Bundle().apply {
|
||||
putString("deep_link", deepLinkUrl)
|
||||
}
|
||||
|
||||
fun putQuery(key: String) {
|
||||
val value = data.getQueryParameter(key)
|
||||
if (!value.isNullOrBlank()) {
|
||||
extras.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
putQuery("room_id")
|
||||
putQuery("channel_id")
|
||||
putQuery("message_id")
|
||||
putQuery("audition_id")
|
||||
putQuery("content_id")
|
||||
putQuery("deep_link_value")
|
||||
putQuery("deep_link_sub5")
|
||||
putQuery("postId")
|
||||
putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID)
|
||||
putQuery(Constants.EXTRA_COMMUNITY_POST_ID)
|
||||
|
||||
extras.getString("postId")?.takeIf { it.isNotBlank() }?.let {
|
||||
if (!extras.containsKey(Constants.EXTRA_COMMUNITY_POST_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
applyPathDeepLink(data = data) { key, value ->
|
||||
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
|
||||
extras.putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
val deepLinkValue = extras.getString("deep_link_value")
|
||||
val deepLinkValueId = extras.getString("deep_link_sub5")
|
||||
|
||||
if (!deepLinkValue.isNullOrBlank() && !deepLinkValueId.isNullOrBlank()) {
|
||||
when (deepLinkValue.lowercase(Locale.ROOT)) {
|
||||
"live" -> if (!extras.containsKey("room_id")) {
|
||||
extras.putString("room_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"channel" -> if (!extras.containsKey("channel_id")) {
|
||||
extras.putString("channel_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"content" -> if (!extras.containsKey("content_id")) {
|
||||
extras.putString("content_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"audition" -> if (!extras.containsKey("audition_id")) {
|
||||
extras.putString("audition_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
"community" -> if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) {
|
||||
extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||
}
|
||||
|
||||
"message" -> if (!extras.containsKey("message_id")) {
|
||||
extras.putString("message_id", deepLinkValueId)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
private fun applyPathDeepLink(
|
||||
data: Uri,
|
||||
putIfAbsent: (key: String, value: String?) -> Unit
|
||||
) {
|
||||
val host = data.host?.lowercase(Locale.ROOT).orEmpty()
|
||||
val pathSegments = data.pathSegments.filter { it.isNotBlank() }
|
||||
|
||||
val pathType: String
|
||||
val pathId: String?
|
||||
|
||||
if (host.isNotBlank() && host != "payverse") {
|
||||
pathType = host
|
||||
pathId = pathSegments.firstOrNull()
|
||||
} else if (pathSegments.isNotEmpty()) {
|
||||
pathType = pathSegments[0].lowercase(Locale.ROOT)
|
||||
pathId = pathSegments.getOrNull(1)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
when (pathType) {
|
||||
"live" -> {
|
||||
putIfAbsent("room_id", pathId)
|
||||
putIfAbsent("deep_link_value", "live")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
putIfAbsent("content_id", pathId)
|
||||
putIfAbsent("deep_link_value", "content")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"series" -> {
|
||||
putIfAbsent("deep_link_value", "series")
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"community" -> {
|
||||
putIfAbsent("deep_link_value", "community")
|
||||
putIfAbsent(Constants.EXTRA_COMMUNITY_CREATOR_ID, pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"message" -> {
|
||||
putIfAbsent("deep_link_value", "message")
|
||||
putIfAbsent("message_id", pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
|
||||
"audition" -> {
|
||||
putIfAbsent("deep_link_value", "audition")
|
||||
putIfAbsent("audition_id", pathId)
|
||||
putIfAbsent("deep_link_sub5", pathId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeOneLink() {
|
||||
val deepLinkValue = SharedPreferenceManager.marketingLinkValue
|
||||
val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId
|
||||
|
||||
if (deepLinkValue.isNotBlank() && deepLinkValueId > 0) {
|
||||
when (deepLinkValue) {
|
||||
"series" -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
startActivity(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
"channel" -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
"live" -> {
|
||||
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
||||
|
||||
handler.postDelayed({
|
||||
liveFragment.enterLiveRoom(deepLinkValueId)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
if (deepLinkValue.isNotBlank()) {
|
||||
routeByDeepLinkValue(
|
||||
deepLinkValue = deepLinkValue,
|
||||
deepLinkValueId = deepLinkValueId.takeIf { it > 0 }
|
||||
)
|
||||
}
|
||||
|
||||
clearDeferredDeepLink()
|
||||
}
|
||||
|
||||
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long?): Boolean {
|
||||
return when (deepLinkValue.lowercase(Locale.ROOT)) {
|
||||
"series" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"channel" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"live" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
|
||||
|
||||
handler.postDelayed({
|
||||
liveFragment.enterLiveRoom(deepLinkValueId)
|
||||
}, 500)
|
||||
true
|
||||
}
|
||||
|
||||
"community" -> {
|
||||
if (deepLinkValueId == null || deepLinkValueId <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
"message" -> {
|
||||
startActivity(Intent(applicationContext, MessageActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
"audition" -> {
|
||||
startActivity(Intent(applicationContext, AuditionActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDeferredDeepLink() {
|
||||
SharedPreferenceManager.marketingUtmSource = ""
|
||||
SharedPreferenceManager.marketingUtmMedium = ""
|
||||
|
||||
@@ -12,8 +12,9 @@ data class MyPageResponse(
|
||||
@SerializedName("point") val point: Int,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String?,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String?,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String?,
|
||||
@SerializedName("blogUrl") val blogUrl: String?,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String?,
|
||||
@SerializedName("xUrl") val xUrl: String?,
|
||||
@SerializedName("liveReservationCount") val liveReservationCount: Int,
|
||||
@SerializedName("likeCount") val likeCount: Int,
|
||||
@SerializedName("isAuth") val isAuth: Boolean
|
||||
|
||||
@@ -15,8 +15,9 @@ data class ProfileResponse(
|
||||
@SerializedName("rewardCan") val rewardCan: Int,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String?,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String?,
|
||||
@SerializedName("blogUrl") val blogUrl: String?,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String?,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String?,
|
||||
@SerializedName("xUrl") val xUrl: String?,
|
||||
@SerializedName("introduce") val introduce: String,
|
||||
@SerializedName("tags") val tags: List<String>
|
||||
)
|
||||
|
||||
@@ -76,22 +76,32 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
|
||||
|
||||
private fun bindData() {
|
||||
compositeDisposable.add(
|
||||
binding.etBlog.textChanges().skip(1)
|
||||
binding.etFancimm.textChanges().skip(1)
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe {
|
||||
viewModel.blogUrl = it.toString()
|
||||
viewModel.fancimmUrl = it.toString()
|
||||
}
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etWebsite.textChanges().skip(1)
|
||||
binding.etX.textChanges().skip(1)
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe {
|
||||
viewModel.websiteUrl = it.toString()
|
||||
viewModel.xUrl = it.toString()
|
||||
}
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etOpenChat.textChanges().skip(1)
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe {
|
||||
viewModel.kakaoOpenChatUrl = it.toString()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,8 +150,9 @@ class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
|
||||
binding.tvNickname.text = it.nickname
|
||||
it.youtubeUrl?.let { url -> binding.etYoutube.setText(url) }
|
||||
it.instagramUrl?.let { url -> binding.etInstagram.setText(url) }
|
||||
it.websiteUrl?.let { url -> binding.etWebsite.setText(url) }
|
||||
it.blogUrl?.let { url -> binding.etBlog.setText(url) }
|
||||
it.kakaoOpenChatUrl?.let { url -> binding.etOpenChat.setText(url) }
|
||||
it.fancimmUrl?.let { url -> binding.etFancimm.setText(url) }
|
||||
it.xUrl?.let { url -> binding.etX.setText(url) }
|
||||
binding.etIntroduce.setText(it.introduce)
|
||||
|
||||
SharedPreferenceManager.nickname = it.nickname
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.mypage.profile
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.explorer.profile.donation.DonationRankingPeriod
|
||||
import kr.co.vividnext.sodalive.user.Gender
|
||||
|
||||
@Keep
|
||||
@@ -16,8 +17,10 @@ data class ProfileUpdateRequest(
|
||||
@SerializedName("introduce") val introduce: String? = null,
|
||||
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
|
||||
@SerializedName("instagramUrl") val instagramUrl: String? = null,
|
||||
@SerializedName("websiteUrl") val websiteUrl: String? = null,
|
||||
@SerializedName("blogUrl") val blogUrl: String? = null,
|
||||
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String? = null,
|
||||
@SerializedName("fancimmUrl") val fancimmUrl: String? = null,
|
||||
@SerializedName("xUrl") val xUrl: String? = null,
|
||||
@SerializedName("isVisibleDonationRank") val isVisibleDonationRank: Boolean? = null,
|
||||
@SerializedName("donationRankingPeriod") val donationRankingPeriod: DonationRankingPeriod? = null,
|
||||
@SerializedName("container") val container: String = "aos"
|
||||
)
|
||||
|
||||
@@ -20,8 +20,9 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
|
||||
|
||||
var youtubeUrl = ""
|
||||
var instagramUrl = ""
|
||||
var websiteUrl = ""
|
||||
var blogUrl = ""
|
||||
var kakaoOpenChatUrl = ""
|
||||
var fancimmUrl = ""
|
||||
var xUrl = ""
|
||||
var introduce = ""
|
||||
|
||||
var currentPassword = ""
|
||||
@@ -63,6 +64,12 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
profileResponse = it.data
|
||||
youtubeUrl = profileResponse.youtubeUrl ?: ""
|
||||
instagramUrl = profileResponse.instagramUrl ?: ""
|
||||
kakaoOpenChatUrl = profileResponse.kakaoOpenChatUrl ?: ""
|
||||
fancimmUrl = profileResponse.fancimmUrl ?: ""
|
||||
xUrl = profileResponse.xUrl ?: ""
|
||||
introduce = profileResponse.introduce
|
||||
tags.addAll(profileResponse.tags)
|
||||
_selectedTagLiveData.postValue(profileResponse.tags)
|
||||
_genderLiveData.postValue(profileResponse.gender)
|
||||
@@ -125,8 +132,9 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
|
||||
if (
|
||||
profileResponse.youtubeUrl != youtubeUrl ||
|
||||
profileResponse.instagramUrl != instagramUrl ||
|
||||
profileResponse.blogUrl != blogUrl ||
|
||||
profileResponse.websiteUrl != websiteUrl ||
|
||||
profileResponse.kakaoOpenChatUrl != kakaoOpenChatUrl ||
|
||||
profileResponse.fancimmUrl != fancimmUrl ||
|
||||
profileResponse.xUrl != xUrl ||
|
||||
profileResponse.gender != _genderLiveData.value ||
|
||||
insertTags.isNotEmpty() ||
|
||||
removeTags.isNotEmpty() ||
|
||||
@@ -145,13 +153,18 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM
|
||||
} else {
|
||||
null
|
||||
},
|
||||
blogUrl = if (profileResponse.blogUrl != blogUrl) {
|
||||
blogUrl
|
||||
kakaoOpenChatUrl = if (profileResponse.kakaoOpenChatUrl != kakaoOpenChatUrl) {
|
||||
kakaoOpenChatUrl
|
||||
} else {
|
||||
null
|
||||
},
|
||||
websiteUrl = if (profileResponse.websiteUrl != websiteUrl) {
|
||||
websiteUrl
|
||||
fancimmUrl = if (profileResponse.fancimmUrl != fancimmUrl) {
|
||||
fancimmUrl
|
||||
} else {
|
||||
null
|
||||
},
|
||||
xUrl = if (profileResponse.xUrl != xUrl) {
|
||||
xUrl
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.settings.language
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
object LanguageManager {
|
||||
const val LANG_KO = "ko"
|
||||
const val LANG_EN = "en"
|
||||
const val LANG_JA = "ja"
|
||||
|
||||
private const val PREF_KEY_APP_LANGUAGE = "pref_app_language_code"
|
||||
|
||||
private fun prefs(context: Context): SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||
|
||||
fun isSupported(code: String): Boolean = when (code) {
|
||||
LANG_KO, LANG_EN, LANG_JA -> true
|
||||
else -> false
|
||||
@@ -25,8 +18,9 @@ object LanguageManager {
|
||||
* 사용자가 앱 내에서 명시적으로 선택한 언어 코드를 반환한다. 없으면 null.
|
||||
*/
|
||||
fun getUserSelectedLanguageOrNull(context: Context): String? {
|
||||
val code = prefs(context).getString(PREF_KEY_APP_LANGUAGE, null)
|
||||
return code?.takeIf { it.isNotBlank() }
|
||||
SharedPreferenceManager.init(context.applicationContext)
|
||||
val code = SharedPreferenceManager.appLanguageCode
|
||||
return code.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +58,7 @@ object LanguageManager {
|
||||
|
||||
fun setSelectedLanguage(context: Context, code: String) {
|
||||
val normalized = if (isSupported(code)) code else LANG_KO
|
||||
prefs(context).edit { putString(PREF_KEY_APP_LANGUAGE, normalized) }
|
||||
SharedPreferenceManager.init(context.applicationContext)
|
||||
SharedPreferenceManager.appLanguageCode = normalized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package kr.co.vividnext.sodalive.settings.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityNotificationReceiveSettingsBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.following.FollowingCreatorAdapter
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationReceiveSettingsBinding>(
|
||||
ActivityNotificationReceiveSettingsBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: NotificationReceiveSettingsViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: FollowingCreatorAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
bindData()
|
||||
|
||||
viewModel.getNotificationSettings()
|
||||
viewModel.getFollowedCreatorAllList()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
binding.toolbar.tvBack.text = getString(R.string.screen_notification_receive_settings_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
binding.ivNotifiedLive.setOnClickListener {
|
||||
viewModel.toggleFollowingChannelLiveNotice()
|
||||
}
|
||||
|
||||
binding.ivNotifiedUploadContent.setOnClickListener {
|
||||
viewModel.toggleFollowingChannelUploadContentNotice()
|
||||
}
|
||||
|
||||
binding.ivMessage.setOnClickListener { viewModel.toggleMessage() }
|
||||
|
||||
setupFollowingChannels()
|
||||
}
|
||||
|
||||
private fun setupFollowingChannels() {
|
||||
adapter = FollowingCreatorAdapter(
|
||||
onClickItem = { creatorId ->
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, creatorId)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickFollow = { creatorId, isFollow ->
|
||||
if (isFollow) {
|
||||
val notifyFragment = CreatorFollowNotifyFragment(
|
||||
onClickNotifyAll = {
|
||||
viewModel.follow(creatorId, follow = true, notify = true)
|
||||
},
|
||||
onClickNotifyNone = {
|
||||
viewModel.follow(creatorId, follow = true, notify = false)
|
||||
},
|
||||
onClickUnFollow = {
|
||||
viewModel.follow(creatorId, follow = false, notify = false)
|
||||
}
|
||||
)
|
||||
|
||||
if (notifyFragment.isAdded) return@FollowingCreatorAdapter
|
||||
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
|
||||
} else {
|
||||
viewModel.follow(creatorId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvFollowingChannel.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvFollowingChannel.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvFollowingChannel.adapter = adapter
|
||||
|
||||
binding.nsvContent.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
||||
val contentView = binding.nsvContent.getChildAt(0) ?: return@setOnScrollChangeListener
|
||||
if (scrollY >= contentView.measuredHeight - binding.nsvContent.measuredHeight) {
|
||||
viewModel.getFollowedCreatorAllList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun bindData() {
|
||||
viewModel.followingChannelLiveNotice.observe(this) {
|
||||
binding.ivNotifiedLive.setImageResource(
|
||||
if (it) {
|
||||
R.drawable.btn_toggle_on_big
|
||||
} else {
|
||||
R.drawable.btn_toggle_off_big
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.followingChannelUploadContentNotice.observe(this) {
|
||||
binding.ivNotifiedUploadContent.setImageResource(
|
||||
if (it) {
|
||||
R.drawable.btn_toggle_on_big
|
||||
} else {
|
||||
R.drawable.btn_toggle_off_big
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.isMessage.observe(this) {
|
||||
binding.ivMessage.setImageResource(
|
||||
if (it) {
|
||||
R.drawable.btn_toggle_on_big
|
||||
} else {
|
||||
R.drawable.btn_toggle_off_big
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.creatorListLiveData.observe(this) {
|
||||
if (viewModel.page == 2) adapter.clear()
|
||||
adapter.addAll(it)
|
||||
}
|
||||
|
||||
viewModel.creatorListTotalCountLiveData.observe(this) {
|
||||
binding.tvTotalCount.text = " ${getString(
|
||||
R.string.following_creator_total_count_value,
|
||||
it
|
||||
)} "
|
||||
|
||||
if (it > 0) {
|
||||
binding.tvNone.visibility = View.GONE
|
||||
binding.rvFollowingChannel.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvNone.visibility = View.VISIBLE
|
||||
binding.rvFollowingChannel.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
val text = it?.message ?: it?.resId?.let { resId -> getString(resId) }
|
||||
text?.let { message ->
|
||||
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package kr.co.vividnext.sodalive.settings.notification
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
|
||||
import kr.co.vividnext.sodalive.following.GetCreatorFollowingAllListItem
|
||||
import kr.co.vividnext.sodalive.user.UserRepository
|
||||
|
||||
class NotificationReceiveSettingsViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val followingCreatorRepository: FollowingCreatorRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _toastLiveData = MutableLiveData<ToastMessage?>()
|
||||
val toastLiveData: LiveData<ToastMessage?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private val _followingChannelLiveNotice = MutableLiveData(false)
|
||||
val followingChannelLiveNotice: LiveData<Boolean>
|
||||
get() = _followingChannelLiveNotice
|
||||
|
||||
private val _isMessage = MutableLiveData(false)
|
||||
val isMessage: LiveData<Boolean>
|
||||
get() = _isMessage
|
||||
|
||||
private val _followingChannelUploadContentNotice = MutableLiveData(false)
|
||||
val followingChannelUploadContentNotice: LiveData<Boolean>
|
||||
get() = _followingChannelUploadContentNotice
|
||||
|
||||
private val _creatorListLiveData = MutableLiveData<List<GetCreatorFollowingAllListItem>>()
|
||||
val creatorListLiveData: LiveData<List<GetCreatorFollowingAllListItem>>
|
||||
get() = _creatorListLiveData
|
||||
|
||||
private val _creatorListTotalCountLiveData = MutableLiveData(0)
|
||||
val creatorListTotalCountLiveData: LiveData<Int>
|
||||
get() = _creatorListTotalCountLiveData
|
||||
|
||||
private var _isFollowingListLoading = MutableLiveData(false)
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private var pageSize = PAGE_SIZE
|
||||
|
||||
fun getNotificationSettings() {
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
|
||||
if (it.success && it.data != null) {
|
||||
val data = it.data
|
||||
_isMessage.value = data.messageNotice ?: false
|
||||
_followingChannelUploadContentNotice.value =
|
||||
data.followingChannelUploadContentNotice ?: false
|
||||
_followingChannelLiveNotice.value =
|
||||
data.followingChannelLiveNotice ?: false
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(ToastMessage(message = it.message))
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(ToastMessage(resId = R.string.retry))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleFollowingChannelLiveNotice() {
|
||||
val nextValue = !(_followingChannelLiveNotice.value ?: false)
|
||||
_followingChannelLiveNotice.value = nextValue
|
||||
updateNotificationSettings(live = nextValue)
|
||||
}
|
||||
|
||||
fun toggleFollowingChannelUploadContentNotice() {
|
||||
val nextValue = !(_followingChannelUploadContentNotice.value ?: false)
|
||||
_followingChannelUploadContentNotice.value = nextValue
|
||||
updateNotificationSettings(uploadContent = nextValue)
|
||||
}
|
||||
|
||||
fun toggleMessage() {
|
||||
val nextValue = !(_isMessage.value ?: false)
|
||||
_isMessage.value = nextValue
|
||||
updateNotificationSettings(message = nextValue)
|
||||
}
|
||||
|
||||
private fun updateNotificationSettings(
|
||||
live: Boolean? = null,
|
||||
uploadContent: Boolean? = null,
|
||||
message: Boolean? = null
|
||||
) {
|
||||
if (live != null || uploadContent != null || message != null) {
|
||||
compositeDisposable.add(
|
||||
userRepository.updateNotificationSettings(
|
||||
request = UpdateNotificationSettingRequest(
|
||||
live = live,
|
||||
uploadContent = uploadContent,
|
||||
message = message
|
||||
),
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({}, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFollowedCreatorAllList() {
|
||||
if (isLast || _isFollowingListLoading.value == true) {
|
||||
return
|
||||
}
|
||||
|
||||
_isFollowingListLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
followingCreatorRepository.getFollowedCreatorAllList(
|
||||
page = page,
|
||||
size = pageSize,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isFollowingListLoading.value = false
|
||||
pageSize = PAGE_SIZE
|
||||
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isEmpty()) {
|
||||
isLast = true
|
||||
} else {
|
||||
page += 1
|
||||
_creatorListTotalCountLiveData.value = it.data.totalCount
|
||||
_creatorListLiveData.value = it.data.items
|
||||
}
|
||||
} else if (it.message != null) {
|
||||
_toastLiveData.postValue(ToastMessage(message = it.message))
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
_isFollowingListLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
userRepository.creatorFollow(
|
||||
creatorId = creatorId,
|
||||
follow = follow,
|
||||
notify = notify,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
|
||||
if (it.success && it.data != null) {
|
||||
isLast = false
|
||||
pageSize *= page
|
||||
page = 1
|
||||
getFollowedCreatorAllList()
|
||||
} else if (it.message != null) {
|
||||
_toastLiveData.postValue(ToastMessage(message = it.message))
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(ToastMessage(resId = R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.user
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationCategoryResponse
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||
@@ -47,6 +49,19 @@ interface UserApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetMemberInfoResponse>>
|
||||
|
||||
@GET("/push/notification/categories")
|
||||
fun getPushNotificationCategories(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetPushNotificationCategoryResponse>>
|
||||
|
||||
@GET("/push/notification/list")
|
||||
fun getPushNotificationList(
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("category") category: String?,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetPushNotificationListResponse>>
|
||||
|
||||
@POST("/member/notification")
|
||||
fun updateNotificationSettings(
|
||||
@Body request: UpdateNotificationSettingRequest,
|
||||
@@ -180,5 +195,4 @@ interface UserApi {
|
||||
fun loginLine(
|
||||
@Body request: SocialLoginRequest
|
||||
): Single<ApiResponse<LoginResponse>>
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.user
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationCategoryResponse
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.GetPushNotificationListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
|
||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||
@@ -36,6 +38,24 @@ class UserRepository(private val userApi: UserApi) {
|
||||
|
||||
fun getMemberInfo(token: String) = userApi.getMemberInfo(authHeader = token)
|
||||
|
||||
fun getPushNotificationCategories(token: String): Single<ApiResponse<GetPushNotificationCategoryResponse>> {
|
||||
return userApi.getPushNotificationCategories(authHeader = token)
|
||||
}
|
||||
|
||||
fun getPushNotificationList(
|
||||
page: Int,
|
||||
size: Int,
|
||||
category: String?,
|
||||
token: String
|
||||
): Single<ApiResponse<GetPushNotificationListResponse>> {
|
||||
return userApi.getPushNotificationList(
|
||||
page = page - 1,
|
||||
size = size,
|
||||
category = category,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
|
||||
fun getMyPage(token: String): Single<ApiResponse<MyPageResponse>> {
|
||||
return userApi.getMyPage(authHeader = token)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.user.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -13,10 +15,15 @@ import androidx.credentials.Credential
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.CustomCredential
|
||||
import androidx.credentials.GetCredentialRequest
|
||||
import androidx.credentials.GetCredentialResponse
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.credentials.exceptions.NoCredentialException
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
||||
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
|
||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential.Companion.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
|
||||
import com.jakewharton.rxbinding4.widget.textChanges
|
||||
@@ -43,6 +50,7 @@ import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
||||
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::inflate) {
|
||||
@@ -55,6 +63,8 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var lineLoginNonce: String? = null
|
||||
private val minGooglePlayServicesMajor = 24
|
||||
private val minGooglePlayServicesMinor = 40
|
||||
|
||||
private val lineLoginLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
@@ -77,20 +87,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
identityToken = identityToken,
|
||||
nonce = lineLoginNonce
|
||||
) {
|
||||
finishAffinity()
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(nextIntent)
|
||||
navigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,37 +135,11 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
binding.ivSignUpEmail.setOnClickListener { startSignUp() }
|
||||
|
||||
binding.ivLoginGoogle.setOnClickListener {
|
||||
loadingDialog.show(width = screenWidth)
|
||||
val credentialManager = CredentialManager.create(this)
|
||||
|
||||
val googleIdOption = GetGoogleIdOption.Builder()
|
||||
.setServerClientId(BuildConfig.GOOGLE_CLIENT_ID)
|
||||
.setFilterByAuthorizedAccounts(false)
|
||||
.setAutoSelectEnabled(false)
|
||||
.build()
|
||||
|
||||
// Create the Credential Manager request
|
||||
val request = GetCredentialRequest.Builder()
|
||||
.addCredentialOption(googleIdOption)
|
||||
.build()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Launch Credential Manager UI
|
||||
val result = credentialManager.getCredential(
|
||||
context = this@LoginActivity,
|
||||
request = request
|
||||
)
|
||||
loadingDialog.dismiss()
|
||||
|
||||
// Extract credential from the result returned by Credential Manager
|
||||
handleSignIn(result.credential)
|
||||
} catch (e: GetCredentialException) {
|
||||
showToast(getString(R.string.login_google_failed))
|
||||
Logger.e("Couldn't retrieve user's credentials: ${e.localizedMessage}")
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
startGoogleLogin(forceUseAllAccounts = false)
|
||||
}
|
||||
binding.ivLoginGoogle.setOnLongClickListener {
|
||||
startGoogleLogin(forceUseAllAccounts = true)
|
||||
true
|
||||
}
|
||||
|
||||
binding.ivLoginKakao.setOnClickListener { loginKakao() }
|
||||
@@ -181,20 +152,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
// Create Google ID Token
|
||||
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
|
||||
viewModel.googleLogin(idToken = googleIdTokenCredential.idToken) {
|
||||
finishAffinity()
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(nextIntent)
|
||||
navigateToMain()
|
||||
}
|
||||
} else {
|
||||
showToast(getString(R.string.login_failed))
|
||||
@@ -205,20 +163,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
private fun login() {
|
||||
viewModel.login {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
finishAffinity()
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(nextIntent)
|
||||
navigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,20 +251,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
|
||||
private fun handleKakaoLogin(token: OAuthToken) {
|
||||
viewModel.kakaoLogin(accessToken = token.accessToken) {
|
||||
finishAffinity()
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(nextIntent)
|
||||
navigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,4 +284,169 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
}
|
||||
startActivity(nextIntent)
|
||||
}
|
||||
|
||||
private fun startGoogleLogin(forceUseAllAccounts: Boolean) {
|
||||
if (!isGoogleLoginAvailable()) {
|
||||
return
|
||||
}
|
||||
loadingDialog.show(width = screenWidth)
|
||||
val credentialManager = CredentialManager.create(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = if (forceUseAllAccounts) {
|
||||
getGoogleCredentialWithSignInOption(credentialManager)
|
||||
} else {
|
||||
getGoogleCredentialWithAuthorizedFirst(credentialManager)
|
||||
}
|
||||
handleSignIn(result.credential)
|
||||
} catch (e: GetCredentialException) {
|
||||
showToast(getString(R.string.login_google_failed))
|
||||
Logger.e(
|
||||
"Couldn't retrieve user's credentials: " +
|
||||
"${e.javaClass.simpleName}, ${e.localizedMessage}"
|
||||
)
|
||||
} finally {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGoogleCredentialWithAuthorizedFirst(
|
||||
credentialManager: CredentialManager
|
||||
): GetCredentialResponse {
|
||||
return try {
|
||||
credentialManager.getCredential(
|
||||
context = this@LoginActivity,
|
||||
request = buildGoogleIdRequest(filterByAuthorizedAccounts = true)
|
||||
)
|
||||
} catch (e: NoCredentialException) {
|
||||
Logger.i(
|
||||
"No authorized account. Retry with all accounts: ${e.localizedMessage}"
|
||||
)
|
||||
credentialManager.getCredential(
|
||||
context = this@LoginActivity,
|
||||
request = buildGoogleIdRequest(filterByAuthorizedAccounts = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGoogleCredentialWithSignInOption(
|
||||
credentialManager: CredentialManager
|
||||
): GetCredentialResponse {
|
||||
return credentialManager.getCredential(
|
||||
context = this@LoginActivity,
|
||||
request = buildSignInWithGoogleRequest()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildGoogleIdRequest(filterByAuthorizedAccounts: Boolean): GetCredentialRequest {
|
||||
val googleIdOption = GetGoogleIdOption.Builder()
|
||||
.setServerClientId(BuildConfig.GOOGLE_CLIENT_ID)
|
||||
.setFilterByAuthorizedAccounts(filterByAuthorizedAccounts)
|
||||
.setAutoSelectEnabled(false)
|
||||
.build()
|
||||
|
||||
return GetCredentialRequest.Builder()
|
||||
.addCredentialOption(googleIdOption)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildSignInWithGoogleRequest(): GetCredentialRequest {
|
||||
val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(
|
||||
BuildConfig.GOOGLE_CLIENT_ID
|
||||
).build()
|
||||
|
||||
return GetCredentialRequest.Builder()
|
||||
.addCredentialOption(signInWithGoogleOption)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun isGoogleLoginAvailable(): Boolean {
|
||||
if (BuildConfig.GOOGLE_CLIENT_ID.isBlank()) {
|
||||
Logger.e("Google login blocked: GOOGLE_CLIENT_ID is blank.")
|
||||
showToast(getString(R.string.login_google_failed))
|
||||
return false
|
||||
}
|
||||
|
||||
val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this)
|
||||
if (status == ConnectionResult.SUCCESS) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
||||
isGooglePlayServicesVersionOutdated()
|
||||
) {
|
||||
Logger.e(
|
||||
"Google login blocked: Google Play services outdated for Android 14+."
|
||||
)
|
||||
promptGooglePlayServicesUpdate()
|
||||
showToast(getString(R.string.login_google_failed))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Logger.e("Google login blocked: Google Play services unavailable. status=$status")
|
||||
if (GoogleApiAvailability.getInstance().isUserResolvableError(status)) {
|
||||
GoogleApiAvailability.getInstance()
|
||||
.getErrorDialog(this, status, 1001)?.show()
|
||||
}
|
||||
showToast(getString(R.string.login_google_failed))
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isGooglePlayServicesVersionOutdated(): Boolean {
|
||||
return try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(
|
||||
"com.google.android.gms",
|
||||
android.content.pm.PackageManager.PackageInfoFlags.of(0)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getPackageInfo("com.google.android.gms", 0)
|
||||
}
|
||||
val versionName = packageInfo.versionName ?: return false
|
||||
val match = Regex("""^(\d+)\.(\d+)""").find(versionName) ?: return false
|
||||
val major = match.groupValues[1].toIntOrNull() ?: return false
|
||||
val minor = match.groupValues[2].toIntOrNull() ?: return false
|
||||
major < minGooglePlayServicesMajor ||
|
||||
(major == minGooglePlayServicesMajor && minor < minGooglePlayServicesMinor)
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to read Google Play services version: ${e.localizedMessage}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptGooglePlayServicesUpdate() {
|
||||
val marketIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"market://details?id=com.google.android.gms".toUri()
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val webIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://play.google.com/store/apps/details?id=com.google.android.gms".toUri()
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
try {
|
||||
startActivity(marketIntent)
|
||||
} catch (_: Exception) {
|
||||
startActivity(webIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMain() {
|
||||
val nextIntent = Intent(this@LoginActivity, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
nextIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(nextIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,18 +70,7 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
|
||||
hideKeyboard()
|
||||
viewModel.signUp {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
finishAffinity()
|
||||
val nextIntent = Intent(applicationContext, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
startActivity(nextIntent)
|
||||
navigateToMain()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,4 +150,21 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMain() {
|
||||
val nextIntent = Intent(this@SignUpActivity, MainActivity::class.java)
|
||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||
?: if (intent.extras != null) {
|
||||
intent.extras
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (extras != null) {
|
||||
nextIntent.putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
nextIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(nextIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable-mdpi/ic_message.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 B |
BIN
app/src/main/res/drawable-mdpi/ic_shield.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_shield.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 B |
BIN
app/src/main/res/drawable-xhdpi/ic_live_creator_follow_alarm.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_live_creator_follow_alarm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 703 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_live_creator_follow_plus.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_live_creator_follow_plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_bell.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_bell.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user