Compare commits
52 Commits
14817ef344
...
4457941193
| Author | SHA1 | Date | |
|---|---|---|---|
| 4457941193 | |||
| 3121d9dca9 | |||
| 99b7a6ce99 | |||
| 751b031627 | |||
| 7f0e7b0ff7 | |||
| bf2a5c7489 | |||
| bdc5e8020c | |||
| 698c395f3d | |||
| 65960888aa | |||
| bd3f961ee1 | |||
| 723fe6b90c | |||
| 17fc70d9ee | |||
| fe5af96ff7 | |||
| 5dd58d0092 | |||
| b565a0eb02 | |||
| 4b84e30195 | |||
| ef8b3fed1c | |||
| 003ebbcc52 | |||
| 9f0adc0593 | |||
| 7fe25f474a | |||
| 4ef880c350 | |||
| 8295e3d25e | |||
| d0dd6c9224 | |||
| 712a2e62e9 | |||
| 8f6c837e22 | |||
| 290b15d007 | |||
| 1139040f28 | |||
| ebd72ef739 | |||
| c639eaf86a | |||
| c75b089cb7 | |||
| 4e18205cca | |||
| 31306583d0 | |||
| 4a4cdadef1 | |||
| f6a94e0f7c | |||
| 30b3dcdce6 | |||
| af0d788796 | |||
| 40d8092880 | |||
| 002d20dc0f | |||
| d8221dc784 | |||
| 68e8941cc1 | |||
| 8271f117a4 | |||
| 21e73e013f | |||
| b589329398 | |||
| 272cd502be | |||
| 1288fc3878 | |||
| 84fa71c64f | |||
| f9b0c274b2 | |||
| 9654c41fb9 | |||
| c5411899bc | |||
| 336d411627 | |||
| 8d8d5e340f | |||
| 9ef2cb1731 |
259
AGENTS.md
@@ -1,172 +1,143 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
|
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
|
||||||
|
|
||||||
|
## 실행 우선순위 및 통합 정책
|
||||||
|
- 충돌 시 아래 우선순위가 높은 지시를 항상 우선 적용한다.
|
||||||
|
- 우선순위는 다음과 같다.
|
||||||
|
1. 사용자 직접 지시
|
||||||
|
2. `AGENTS.md`
|
||||||
|
3. 프로젝트별 제약 조건
|
||||||
|
4. oh-my-openagent 플러그인의 agents / workflows / hooks
|
||||||
|
5. superpowers skills
|
||||||
|
6. 기본 모델 동작
|
||||||
|
- plugin / skill / workflow 지시가 더 낮은 우선순위에 있으면 더 높은 우선순위의 지시를 덮어쓸 수 없다.
|
||||||
|
- plugin / skill / workflow 지시가 `CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)`와 충돌하면 `CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)`를 따른다.
|
||||||
|
- 사용자 직접 지시가 명확할 경우 사용자 지시가 최우선이다.
|
||||||
|
|
||||||
## 커뮤니케이션 규칙
|
## 커뮤니케이션 규칙
|
||||||
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||||
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||||
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||||
|
|
||||||
## 저장소 범위
|
## CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)
|
||||||
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
These principles override plugin behavior, skill behavior, workflow behavior, and default model behavior unless the user's direct instruction explicitly says otherwise.
|
||||||
- 모든 명령은 저장소 루트에서 실행한다.
|
|
||||||
- 추측하지 말고 근거 파일(`settings.gradle`, `build.gradle`, `app/build.gradle`, 소스 코드)을 읽고 결정한다.
|
|
||||||
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
|
||||||
|
|
||||||
## 빌드 / 린트 / 테스트 명령
|
# CLAUDE.md
|
||||||
기본 실행 형태:
|
|
||||||
```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) 단일 테스트 실행 (중요)
|
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||||
로컬 단위 테스트(`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) 계측 테스트 클래스/메서드 타깃 실행
|
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||||
Gradle 인자 방식:
|
|
||||||
```bash
|
## 1. Think Before Coding
|
||||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest
|
|
||||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them - don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
## 2. Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||||
|
|
||||||
|
## 3. Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, mention it - don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions that YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
The test: Every changed line should trace directly to the user's request.
|
||||||
|
|
||||||
|
## 4. Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
```
|
```
|
||||||
ADB 대안:
|
1. [Step] → verify: [check]
|
||||||
```bash
|
2. [Step] → verify: [check]
|
||||||
adb shell am instrument -w -e class kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod <test_package>/<runner>
|
3. [Step] → verify: [check]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 코드 스타일 가이드
|
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||||
### 1) 포맷/기본 규칙
|
|
||||||
- `.editorconfig` 기준을 준수한다.
|
|
||||||
- 인덴트: 공백 4칸, 줄바꿈: LF, 최대 라인 길이: 130.
|
|
||||||
- 파일 끝 개행 유지, trailing whitespace 제거.
|
|
||||||
- Kotlin/KTS에서 `import-ordering` ktlint 규칙은 비활성화되어 있으므로 기존 파일 정렬 스타일을 우선 따른다.
|
|
||||||
|
|
||||||
### 2) import 규칙
|
---
|
||||||
- 신규 코드에서는 와일드카드 import(`*`)를 기본적으로 지양한다.
|
|
||||||
- 사용하지 않는 import를 남기지 않는다.
|
|
||||||
- import alias(`as`)는 필요한 경우(이름 충돌 회피) 최소 범위로만 사용한다.
|
|
||||||
- 기존 파일에 와일드카드/alias가 있으면 대규모 정렬 리팩터링 없이 주변 스타일에 맞춘다.
|
|
||||||
|
|
||||||
### 3) 네이밍/레이어
|
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||||
- 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다.
|
- 단순한 구현
|
||||||
- 테스트 추가 시 단일 실행 명령 예시도 본 문서에 갱신한다.
|
- 검증 가능한 결과
|
||||||
|
|
||||||
### 6) 주석
|
### 확장 모드
|
||||||
- 의미 단위별로 주석을 작성한다.
|
- 사용자가 명시적으로 요청한 경우에만 사용한다.
|
||||||
- 주석은 한 문장으로 간결하게 작성한다.
|
- 대규모 리팩토링, 브레인스토밍, 다중 에이전트 실행, 병렬 workflow를 허용한다.
|
||||||
- 주석은 코드의 의도와 구조를 설명한다.
|
|
||||||
- 주석은 코드 변경 시 업데이트를 잊지 않는다.
|
|
||||||
|
|
||||||
## 커밋 메시지 규칙 (표준 Conventional Commits)
|
### oh-my-openagent 사용 정책
|
||||||
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
|
- oh-my-openagent는 opencode의 플러그인 기반 실행 오케스트레이션 계층이다.
|
||||||
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
|
- oh-my-openagent는 의사결정 권한이 아니라 실행 보조 권한만 가진다.
|
||||||
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
|
- 작은 작업에는 multi-agent 실행이나 과도한 workflow를 사용하지 않는다.
|
||||||
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test` 등)를 사용한다.
|
- 병렬 실행은 명확한 이득이 있을 때만 사용한다.
|
||||||
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
|
- 모든 oh-my-openagent 동작은 `CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)`를 따라야 한다.
|
||||||
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
|
|
||||||
|
|
||||||
### 커밋 메시지 검증 절차
|
### superpowers 사용 정책
|
||||||
- `git commit` 직전/직후 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
|
- superpowers는 선택적 스킬 계층이다.
|
||||||
- 스크립트 결과가 `[FAIL]`이면 메시지를 수정한 뒤 다시 검증한다.
|
- superpowers skill은 필요한 경우에만 사용한다.
|
||||||
|
- superpowers가 과도한 리팩토링, 불필요한 범위 확장, 가정 기반 실행을 유도하면 따르지 않는다.
|
||||||
|
- superpowers를 사용할 때도 최소 변경, 단순성, 검증 가능성을 우선한다.
|
||||||
|
- 모든 superpowers 동작은 `CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)`를 따라야 한다.
|
||||||
|
|
||||||
## 작업 절차 체크리스트
|
### 에이전트 동작 원칙
|
||||||
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
|
||||||
- 변경 중: 공개 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`)는 생성/수정 여부와 관계없이 커밋하지 않는다.
|
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
||||||
- `app/src/debug/google-services.json`, `app/src/release/google-services.json`은 민감 구성으로 취급하고 외부 공유/로그 출력 금지한다.
|
- 모든 명령은 저장소 루트에서 실행한다.
|
||||||
- `app/build.gradle`의 `buildConfigField` 값(토큰/앱키/시크릿 유사 값)은 신규 하드코딩을 추가하지 않는다.
|
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
||||||
|
- 기존 로직 수정이 아닌 신규 `Activity`, `Fragment`, `ViewModel` 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||||
|
|
||||||
|
## 작업 절차 핵심 규칙
|
||||||
|
- PRD 문서와 구현 계획/TASK 문서 없이 구현하지 않는다.
|
||||||
|
- 사용자의 프롬프트를 받으면 먼저 PRD 문서를 작성하고, 애매하거나 결정이 필요한 내용은 모호함이 사라질 때까지 사용자와 인터뷰한다.
|
||||||
|
- 인터뷰 내용을 PRD에 반영한 뒤, PRD를 기준으로 계획/TASK 문서를 작성하고 그 문서에 따라 필요한 내용만 최소 구현한다.
|
||||||
|
- PRD 문서는 `docs/prd/`, 계획/TASK 문서는 `docs/plan-task/` 아래에 둔다.
|
||||||
|
|
||||||
|
## 상세 참조 문서
|
||||||
|
- 빌드/린트/테스트는 `docs/agent-guides/build-test-style.md`를 참고한다.
|
||||||
|
- 코드 스타일/구조는 `docs/agent-guides/code-style.md`를 참고한다.
|
||||||
|
- 작업 절차/docs/커밋 규칙은 `docs/agent-guides/workflow-docs-commits.md`를 참고한다.
|
||||||
|
- 저장소 세부 규칙/보안/Git 안전 수칙은 `docs/agent-guides/safety-repo-rules.md`를 참고한다.
|
||||||
|
|
||||||
|
## 핵심 금지사항
|
||||||
|
- `local.properties`, 키스토어, Google Services 파일, 비밀값은 커밋하거나 외부에 노출하지 않는다.
|
||||||
- `BuildConfig` 값(키/토큰/URL)을 로그, Toast, 크래시 메시지에 직접 노출하지 않는다.
|
- `BuildConfig` 값(키/토큰/URL)을 로그, Toast, 크래시 메시지에 직접 노출하지 않는다.
|
||||||
- 네트워크 로깅은 `AppDI.kt` 패턴을 유지한다(디버그만 BODY, 릴리스는 NONE).
|
|
||||||
- 서명/배포 설정(Crashlytics, Google Services, Proguard, signing)은 요청 없이 변경하지 않는다.
|
|
||||||
- `AndroidManifest.xml` 권한은 민감 영역이므로 신규 추가/확장은 사유와 영향도를 확인한 뒤 반영한다.
|
|
||||||
- `applicationId`, `namespace`, OAuth Client ID, 딥링크 호스트는 요청 없이 변경하지 않는다.
|
|
||||||
- 문서/이슈/PR 본문에 비밀값을 남기지 말고 필요 시 마스킹(`***`) 처리한다.
|
|
||||||
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
|
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ android {
|
|||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
|
// release용 ad unit id는 배포 전 실제 값으로 교체한다.
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140295-3"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140295-4"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140295-5"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140295-6"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140295-7"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140295-8"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140295-9"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140295-10"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140295-11"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140295-12"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140295-13"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140295-14"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140295-15"'
|
||||||
|
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140295-16"'
|
||||||
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
|
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_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||||
@@ -103,6 +118,20 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
|
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140297-3"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140297-4"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140297-5"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140297-6"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140297-7"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140297-8"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140297-9"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140297-10"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140297-11"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140297-12"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140297-13"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140297-14"'
|
||||||
|
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140297-15"'
|
||||||
|
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140297-16"'
|
||||||
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
|
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_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||||
@@ -156,7 +185,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// image library
|
// image library
|
||||||
implementation "io.coil-kt:coil:1.4.0"
|
implementation "io.coil-kt:coil:2.7.0"
|
||||||
|
|
||||||
// Koin DI
|
// Koin DI
|
||||||
implementation "io.insert-koin:koin-android:3.1.3"
|
implementation "io.insert-koin:koin-android:3.1.3"
|
||||||
@@ -232,6 +261,9 @@ dependencies {
|
|||||||
// Appsflyer
|
// Appsflyer
|
||||||
implementation 'com.appsflyer:af-android-sdk:6.17.4'
|
implementation 'com.appsflyer:af-android-sdk:6.17.4'
|
||||||
|
|
||||||
|
// Yandex Mobile Ads
|
||||||
|
implementation 'com.yandex.android:mobileads:7.18.5'
|
||||||
|
|
||||||
// 노티플라이
|
// 노티플라이
|
||||||
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
|
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".main.MainActivity" />
|
<activity android:name=".main.MainActivity" />
|
||||||
|
<activity android:name=".v2.main.MainV2Activity" />
|
||||||
<activity android:name=".user.login.LoginActivity" />
|
<activity android:name=".user.login.LoginActivity" />
|
||||||
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
||||||
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.facebook.FacebookSdk
|
|||||||
import com.kakao.sdk.common.KakaoSdk
|
import com.kakao.sdk.common.KakaoSdk
|
||||||
import com.orhanobut.logger.AndroidLogAdapter
|
import com.orhanobut.logger.AndroidLogAdapter
|
||||||
import com.orhanobut.logger.Logger
|
import com.orhanobut.logger.Logger
|
||||||
|
import com.yandex.mobile.ads.common.MobileAds
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||||
@@ -52,6 +53,8 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
|||||||
setupAppsFlyer()
|
setupAppsFlyer()
|
||||||
|
|
||||||
setupNotifly()
|
setupNotifly()
|
||||||
|
|
||||||
|
setupYandexAd()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDebuggable(): Boolean {
|
private fun isDebuggable(): Boolean {
|
||||||
@@ -137,6 +140,10 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupYandexAd() {
|
||||||
|
MobileAds.enableDebugErrorIndicator(BuildConfig.DEBUG && isDebuggable())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
super.onStart(owner)
|
super.onStart(owner)
|
||||||
isAppInForeground = true
|
isAppInForeground = true
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import com.bumptech.glide.request.transition.Transition
|
|||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class AudioContentPlayService :
|
class AudioContentPlayService :
|
||||||
@@ -471,7 +471,7 @@ class AudioContentPlayService :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification() {
|
private fun updateNotification() {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ import coil.transform.RoundedCornersTransformation
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdError
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestConfiguration
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestError
|
||||||
|
import com.yandex.mobile.ads.common.ImpressionData
|
||||||
|
import com.yandex.mobile.ads.interstitial.InterstitialAd
|
||||||
|
import com.yandex.mobile.ads.interstitial.InterstitialAdEventListener
|
||||||
|
import com.yandex.mobile.ads.interstitial.InterstitialAdLoadListener
|
||||||
|
import com.yandex.mobile.ads.interstitial.InterstitialAdLoader
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||||
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
|
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
|
||||||
@@ -60,6 +72,7 @@ import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
|||||||
import kr.co.vividnext.sodalive.report.ReportType
|
import kr.co.vividnext.sodalive.report.ReportType
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
|
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
|
||||||
@@ -88,6 +101,42 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
private lateinit var orderType: OrderType
|
private lateinit var orderType: OrderType
|
||||||
private lateinit var imm: InputMethodManager
|
private lateinit var imm: InputMethodManager
|
||||||
|
|
||||||
|
private var audioContentPlayInterstitialAdLoader: InterstitialAdLoader? = null
|
||||||
|
private var audioContentPlayInterstitialAd: InterstitialAd? = null
|
||||||
|
private var pendingAudioContentPlayAction: (() -> Unit)? = null
|
||||||
|
private var hasConsumedAudioContentPlayInterstitialAttempt = false
|
||||||
|
private var isAudioContentPlaying = false
|
||||||
|
private var isAudioContentInterstitialEligible = false
|
||||||
|
private var audioContentStartPlaybackAction: (() -> Unit)? = null
|
||||||
|
|
||||||
|
private val audioContentPlayInterstitialAdLoadListener = object : InterstitialAdLoadListener {
|
||||||
|
override fun onAdLoaded(interstitialAd: InterstitialAd) {
|
||||||
|
clearAudioContentPlayInterstitialAd()
|
||||||
|
audioContentPlayInterstitialAd = interstitialAd
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdFailedToLoad(error: AdRequestError) {
|
||||||
|
Logger.e("Audio content interstitial failed to load: ${error.description}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val audioContentPlayInterstitialAdEventListener = object : InterstitialAdEventListener {
|
||||||
|
override fun onAdShown() = Unit
|
||||||
|
|
||||||
|
override fun onAdFailedToShow(adError: AdError) {
|
||||||
|
Logger.e("Audio content interstitial failed to show: ${adError.description}")
|
||||||
|
continuePendingAudioContentPlayAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdDismissed() {
|
||||||
|
continuePendingAudioContentPlayAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdClicked() = Unit
|
||||||
|
|
||||||
|
override fun onAdImpression(impressionData: ImpressionData?) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
@@ -105,6 +154,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
binding.rlPreviewAlert.visibility = View.GONE
|
binding.rlPreviewAlert.visibility = View.GONE
|
||||||
|
|
||||||
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||||
|
hasConsumedAudioContentPlayInterstitialAttempt = false
|
||||||
|
releaseAudioContentPlayInterstitial()
|
||||||
|
setupAudioContentPlayInterstitial()
|
||||||
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +367,125 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
|
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupAudioContentDetailInlineBanner()
|
||||||
|
setupAudioContentPlayInterstitial()
|
||||||
setupBuyerList()
|
setupBuyerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupAudioContentDetailInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
this@AudioContentDetailActivity,
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAudioContentPlayInterstitial() {
|
||||||
|
val adUnitId = BuildConfig.YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID
|
||||||
|
if (adUnitId.isBlank()) {
|
||||||
|
Logger.e("Audio content interstitial blocked: ad unit id is blank.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
audioContentPlayInterstitialAdLoader = InterstitialAdLoader(this).apply {
|
||||||
|
setAdLoadListener(audioContentPlayInterstitialAdLoadListener)
|
||||||
|
}
|
||||||
|
audioContentPlayInterstitialAdLoader?.loadAd(
|
||||||
|
AdRequestConfiguration.Builder(adUnitId).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playAudioContentWithInterstitialIfAvailable(playAction: () -> Unit) {
|
||||||
|
if (hasConsumedAudioContentPlayInterstitialAttempt || isFinishing || isDestroyed) {
|
||||||
|
playAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val interstitialAd = audioContentPlayInterstitialAd ?: run {
|
||||||
|
playAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConsumedAudioContentPlayInterstitialAttempt = true
|
||||||
|
pendingAudioContentPlayAction = playAction
|
||||||
|
interstitialAd.setAdEventListener(audioContentPlayInterstitialAdEventListener)
|
||||||
|
runCatching {
|
||||||
|
interstitialAd.show(this)
|
||||||
|
}.onFailure {
|
||||||
|
Logger.e("Audio content interstitial failed to show: ${it.message}")
|
||||||
|
continuePendingAudioContentPlayAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun continuePendingAudioContentPlayAction() {
|
||||||
|
val playAction = pendingAudioContentPlayAction
|
||||||
|
pendingAudioContentPlayAction = null
|
||||||
|
clearAudioContentPlayInterstitialAd()
|
||||||
|
if (isFinishing || isDestroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
playAction?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAudioContentPlayInterstitialAd() {
|
||||||
|
audioContentPlayInterstitialAd?.setAdEventListener(null)
|
||||||
|
audioContentPlayInterstitialAd = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun releaseAudioContentPlayInterstitial() {
|
||||||
|
audioContentPlayInterstitialAdLoader?.setAdLoadListener(null)
|
||||||
|
audioContentPlayInterstitialAdLoader = null
|
||||||
|
pendingAudioContentPlayAction = null
|
||||||
|
clearAudioContentPlayInterstitialAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pauseAudioContentPlayback() {
|
||||||
|
startService(
|
||||||
|
Intent(this, AudioContentPlayService::class.java).apply {
|
||||||
|
action = AudioContentPlayService.MusicAction.PAUSE.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateAudioContentPlayOrPauseControls() {
|
||||||
|
val startPlaybackAction = audioContentStartPlaybackAction ?: return
|
||||||
|
|
||||||
|
if (isAudioContentPlaying) {
|
||||||
|
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||||
|
binding.llPreview.visibility = View.GONE
|
||||||
|
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_pause)
|
||||||
|
binding.ivPlayOrPause.setOnClickListener { pauseAudioContentPlayback() }
|
||||||
|
binding.llPreview.setOnClickListener(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
|
||||||
|
|
||||||
|
val startPlaybackClickListener = View.OnClickListener {
|
||||||
|
if (isAudioContentInterstitialEligible) {
|
||||||
|
playAudioContentWithInterstitialIfAvailable(startPlaybackAction)
|
||||||
|
} else {
|
||||||
|
startPlaybackAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.ivPlayOrPause.setOnClickListener(startPlaybackClickListener)
|
||||||
|
binding.llPreview.setOnClickListener(startPlaybackClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupBuyerList() {
|
private fun setupBuyerList() {
|
||||||
val recyclerView = binding.rvBuyer
|
val recyclerView = binding.rvBuyer
|
||||||
contentBuyerAdapter = AudioContentBuyerAdapter()
|
contentBuyerAdapter = AudioContentBuyerAdapter()
|
||||||
@@ -775,10 +943,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
R.string.screen_audio_content_detail_total_duration_format,
|
R.string.screen_audio_content_detail_total_duration_format,
|
||||||
response.duration
|
response.duration
|
||||||
)
|
)
|
||||||
|
audioContentStartPlaybackAction = null
|
||||||
|
isAudioContentInterstitialEligible = false
|
||||||
|
|
||||||
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
|
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
|
||||||
!response.existOrdered &&
|
!response.existOrdered &&
|
||||||
response.price > 0
|
response.price > 0
|
||||||
|
isAudioContentInterstitialEligible = response.price <= 0 || isAlertPreview
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
|
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
|
||||||
@@ -798,7 +969,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
val playClickAction = View.OnClickListener {
|
val playAudioContentAction: () -> Unit = {
|
||||||
startService(
|
startService(
|
||||||
Intent(
|
Intent(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
@@ -842,9 +1013,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioContentStartPlaybackAction = playAudioContentAction
|
||||||
|
|
||||||
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
|
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
|
||||||
binding.ivPlayOrPause.setOnClickListener(playClickAction)
|
updateAudioContentPlayOrPauseControls()
|
||||||
binding.llPreview.setOnClickListener(playClickAction)
|
|
||||||
|
|
||||||
if (!isAlertPreview) {
|
if (!isAlertPreview) {
|
||||||
binding.ivSeekForward10.visibility = View.VISIBLE
|
binding.ivSeekForward10.visibility = View.VISIBLE
|
||||||
@@ -873,6 +1045,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (response.releaseDate == null) {
|
} else if (response.releaseDate == null) {
|
||||||
|
audioContentStartPlaybackAction = null
|
||||||
|
isAudioContentInterstitialEligible = false
|
||||||
binding.llPreviewNo.visibility = View.VISIBLE
|
binding.llPreviewNo.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,7 +1340,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
} else {
|
} else {
|
||||||
contentOrder(audioContent, orderType)
|
contentOrder(audioContent, orderType)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
).show(screenWidth)
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,6 +1367,12 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
releaseAudioContentPlayInterstitial()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
inner class AudioContentReceiver : BroadcastReceiver() {
|
inner class AudioContentReceiver : BroadcastReceiver() {
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -1222,12 +1402,12 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
viewModel.isLoading.value = isLoading == true
|
viewModel.isLoading.value = isLoading == true
|
||||||
|
|
||||||
if (this@AudioContentDetailActivity.audioContentId == contentId) {
|
if (this@AudioContentDetailActivity.audioContentId == contentId) {
|
||||||
|
isAudioContentPlaying = isPlaying == true
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (changeUi != null && changeUi) {
|
if (changeUi != null && changeUi) {
|
||||||
if (isPlaying != null && isPlaying) {
|
if (isPlaying != null && isPlaying) {
|
||||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||||
binding.llPreview.visibility = View.GONE
|
binding.llPreview.visibility = View.GONE
|
||||||
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_pause)
|
|
||||||
} else {
|
} else {
|
||||||
if (isAlertPreview) {
|
if (isAlertPreview) {
|
||||||
binding.ivPlayOrPause.visibility = View.GONE
|
binding.ivPlayOrPause.visibility = View.GONE
|
||||||
@@ -1235,9 +1415,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
|||||||
} else {
|
} else {
|
||||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||||
binding.llPreview.visibility = View.GONE
|
binding.llPreview.visibility = View.GONE
|
||||||
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAudioContentPlayOrPauseControls()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlayli
|
|||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.Utils
|
import kr.co.vividnext.sodalive.common.Utils
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -153,7 +153,7 @@ class AudioContentPlayerService : MediaSessionService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initMediaSession() {
|
private fun initMediaSession() {
|
||||||
val contextIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
import coil.transform.BlurTransformation
|
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
@@ -20,6 +19,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivitySeriesDetailBinding
|
import kr.co.vividnext.sodalive.databinding.ActivitySeriesDetailBinding
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import android.widget.Toast
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
@@ -39,9 +41,36 @@ class SeriesMainByGenreFragment : BaseFragment<FragmentSeriesMainByGenreBinding>
|
|||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
setupGenreView()
|
setupGenreView()
|
||||||
|
setupInlineBanner()
|
||||||
setupSeriesView()
|
setupSeriesView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupGenreView() {
|
private fun setupGenreView() {
|
||||||
genreAdapter = GenreAdapter { genre ->
|
genreAdapter = GenreAdapter { genre ->
|
||||||
seriesAdapter.clear()
|
seriesAdapter.clear()
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import android.widget.Toast
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
@@ -40,6 +43,7 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
|
|||||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
|
|
||||||
setupDayOfWeekDay()
|
setupDayOfWeekDay()
|
||||||
|
setupInlineBanner()
|
||||||
setupSeriesView()
|
setupSeriesView()
|
||||||
|
|
||||||
val dayOfWeeks = listOf(
|
val dayOfWeeks = listOf(
|
||||||
@@ -59,6 +63,32 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
|
|||||||
viewModel.dayOfWeek = dayOfWeeks[dayIndex]
|
viewModel.dayOfWeek = dayOfWeeks[dayIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupDayOfWeekDay() {
|
private fun setupDayOfWeekDay() {
|
||||||
val dayOfWeekAdapter = DayOfWeekAdapter(requireContext()) {
|
val dayOfWeekAdapter = DayOfWeekAdapter(requireContext()) {
|
||||||
adapter.clear()
|
adapter.clear()
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
import com.zhpan.bannerview.BaseBannerAdapter
|
import com.zhpan.bannerview.BaseBannerAdapter
|
||||||
import com.zhpan.indicator.enums.IndicatorSlideMode
|
import com.zhpan.indicator.enums.IndicatorSlideMode
|
||||||
import com.zhpan.indicator.enums.IndicatorStyle
|
import com.zhpan.indicator.enums.IndicatorStyle
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
@@ -54,9 +57,36 @@ class SeriesMainHomeFragment : BaseFragment<FragmentSeriesMainHomeBinding>(
|
|||||||
|
|
||||||
setupBanner()
|
setupBanner()
|
||||||
setupCompletedSeriesView()
|
setupCompletedSeriesView()
|
||||||
|
setupInlineBanner()
|
||||||
setupRecommendSeriesView()
|
setupRecommendSeriesView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupBanner() {
|
private fun setupBanner() {
|
||||||
val layoutParams = binding
|
val layoutParams = binding
|
||||||
.bannerSlider
|
.bannerSlider
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
import com.zhpan.bannerview.BaseBannerAdapter
|
import com.zhpan.bannerview.BaseBannerAdapter
|
||||||
import com.zhpan.indicator.enums.IndicatorSlideMode
|
import com.zhpan.indicator.enums.IndicatorSlideMode
|
||||||
import com.zhpan.indicator.enums.IndicatorStyle
|
import com.zhpan.indicator.enums.IndicatorStyle
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
@@ -37,6 +40,7 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
|||||||
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
|
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
|
||||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
// 캐릭터 탭 프래그먼트
|
// 캐릭터 탭 프래그먼트
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@@ -61,16 +65,43 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
|
|||||||
viewModel.fetchData()
|
viewModel.fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
|
|
||||||
setupBanner()
|
setupBanner()
|
||||||
setupRecentCharactersRecyclerView()
|
setupRecentCharactersRecyclerView()
|
||||||
|
setupCharacterTabInlineBanner()
|
||||||
setupPopularCharactersRecyclerView()
|
setupPopularCharactersRecyclerView()
|
||||||
setupNewCharactersRecyclerView()
|
setupNewCharactersRecyclerView()
|
||||||
setupRecommendCharactersRecyclerView()
|
setupRecommendCharactersRecyclerView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupCharacterTabInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupBanner() {
|
private fun setupBanner() {
|
||||||
val layoutParams = binding
|
val layoutParams = binding
|
||||||
.bannerSlider
|
.bannerSlider
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
@@ -18,6 +20,7 @@ import kr.co.vividnext.sodalive.chat.original.detail.OriginalWorkDetailActivity
|
|||||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.common.YandexInlineBannerHeaderAdapter
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentOriginalTabBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentOriginalTabBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.main.MainActivity
|
||||||
@@ -36,6 +39,7 @@ class OriginalTabFragment :
|
|||||||
private val myPageViewModel: MyPageViewModel by inject()
|
private val myPageViewModel: MyPageViewModel by inject()
|
||||||
|
|
||||||
private lateinit var adapter: OriginalWorkListAdapter
|
private lateinit var adapter: OriginalWorkListAdapter
|
||||||
|
private lateinit var bannerAdapter: YandexInlineBannerHeaderAdapter
|
||||||
|
|
||||||
private lateinit var loadingDialog: LoadingDialog
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
@@ -49,9 +53,19 @@ class OriginalTabFragment :
|
|||||||
viewModel.loadMore()
|
viewModel.loadMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
bannerAdapter.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecycler() {
|
private fun setupRecycler() {
|
||||||
val spanCount = 3
|
val spanCount = 3
|
||||||
val spacingPx = 16f.dpToPx().toInt()
|
val spacingPx = 16f.dpToPx().toInt()
|
||||||
|
val headerCount = 1
|
||||||
|
bannerAdapter = YandexInlineBannerHeaderAdapter(
|
||||||
|
adUnitId = BuildConfig.YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID,
|
||||||
|
screenWidth = screenWidth
|
||||||
|
)
|
||||||
adapter = OriginalWorkListAdapter { id ->
|
adapter = OriginalWorkListAdapter { id ->
|
||||||
ensureLoginAndAuth {
|
ensureLoginAndAuth {
|
||||||
startActivity(
|
startActivity(
|
||||||
@@ -64,12 +78,20 @@ class OriginalTabFragment :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.rvOriginal.layoutManager = GridLayoutManager(requireContext(), spanCount)
|
binding.rvOriginal.setPadding(0, 0, 0, 8f.dpToPx().toInt())
|
||||||
|
binding.rvOriginal.layoutManager = GridLayoutManager(requireContext(), spanCount).apply {
|
||||||
|
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
return if (position < headerCount) spanCount else 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.rvOriginal.addItemDecoration(
|
binding.rvOriginal.addItemDecoration(
|
||||||
GridSpacingItemDecoration(
|
GridSpacingItemDecoration(
|
||||||
spanCount,
|
spanCount,
|
||||||
spacingPx,
|
spacingPx,
|
||||||
true
|
true,
|
||||||
|
headerCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
binding.rvOriginal.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.rvOriginal.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
@@ -83,7 +105,7 @@ class OriginalTabFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
binding.rvOriginal.adapter = adapter
|
binding.rvOriginal.adapter = ConcatAdapter(bannerAdapter, adapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bind() {
|
private fun bind() {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
import coil.transform.BlurTransformation
|
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityOriginalWorkDetailBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityOriginalWorkDetailBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import android.graphics.Rect
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.YandexInlineBannerHeaderAdapter
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentTalkTabBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentTalkTabBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@@ -20,6 +23,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
|||||||
private val viewModel: TalkTabViewModel by inject()
|
private val viewModel: TalkTabViewModel by inject()
|
||||||
|
|
||||||
private lateinit var adapter: TalkTabAdapter
|
private lateinit var adapter: TalkTabAdapter
|
||||||
|
private lateinit var bannerAdapter: YandexInlineBannerHeaderAdapter
|
||||||
|
|
||||||
private lateinit var loadingDialog: LoadingDialog
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
@@ -34,7 +38,17 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
|||||||
viewModel.refreshTalkRooms()
|
viewModel.refreshTalkRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
bannerAdapter.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
|
val headerCount = 1
|
||||||
|
bannerAdapter = YandexInlineBannerHeaderAdapter(
|
||||||
|
adUnitId = BuildConfig.YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID,
|
||||||
|
screenWidth = screenWidth
|
||||||
|
)
|
||||||
adapter = TalkTabAdapter {
|
adapter = TalkTabAdapter {
|
||||||
startActivity(
|
startActivity(
|
||||||
ChatRoomActivity.newIntent(
|
ChatRoomActivity.newIntent(
|
||||||
@@ -56,16 +70,28 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
|||||||
) {
|
) {
|
||||||
super.getItemOffsets(outRect, view, parent, state)
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
if (position < headerCount) {
|
||||||
|
outRect.set(0, 0, 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val adjustedPosition = position - headerCount
|
||||||
|
val lastItemPosition = adapter.itemCount - 1
|
||||||
|
|
||||||
outRect.left = 24f.dpToPx().toInt()
|
outRect.left = 24f.dpToPx().toInt()
|
||||||
outRect.right = 24f.dpToPx().toInt()
|
outRect.right = 24f.dpToPx().toInt()
|
||||||
|
|
||||||
when (parent.getChildAdapterPosition(view)) {
|
when (adjustedPosition) {
|
||||||
0 -> {
|
0 -> {
|
||||||
outRect.top = 24f.dpToPx().toInt()
|
outRect.bottom = if (adjustedPosition == lastItemPosition) {
|
||||||
outRect.bottom = 12f.dpToPx().toInt()
|
24f.dpToPx().toInt()
|
||||||
|
} else {
|
||||||
|
12f.dpToPx().toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.itemCount - 1 -> {
|
lastItemPosition -> {
|
||||||
outRect.top = 12f.dpToPx().toInt()
|
outRect.top = 12f.dpToPx().toInt()
|
||||||
outRect.bottom = 24f.dpToPx().toInt()
|
outRect.bottom = 24f.dpToPx().toInt()
|
||||||
}
|
}
|
||||||
@@ -81,7 +107,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
|||||||
recyclerView.apply {
|
recyclerView.apply {
|
||||||
val lm = LinearLayoutManager(requireContext())
|
val lm = LinearLayoutManager(requireContext())
|
||||||
layoutManager = lm
|
layoutManager = lm
|
||||||
adapter = this@TalkTabFragment.adapter
|
adapter = ConcatAdapter(bannerAdapter, this@TalkTabFragment.adapter)
|
||||||
|
|
||||||
// 스크롤 로딩 리스너: 끝에 도달하면 다음 페이지 로드
|
// 스크롤 로딩 리스너: 끝에 도달하면 다음 페이지 로드
|
||||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
@@ -109,7 +135,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
|||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
} else {
|
} else {
|
||||||
adapter.submitList(emptyList())
|
adapter.submitList(emptyList())
|
||||||
binding.rvTalk.visibility = View.GONE
|
binding.rvTalk.visibility = View.VISIBLE
|
||||||
binding.tvEmpty.visibility = View.VISIBLE
|
binding.tvEmpty.visibility = View.VISIBLE
|
||||||
binding.tvEmpty.setText(R.string.screen_chat_talk_empty)
|
binding.tvEmpty.setText(R.string.screen_chat_talk_empty)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package kr.co.vividnext.sodalive.chat.talk.room
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@@ -37,10 +36,16 @@ sealed class ChatListItem {
|
|||||||
data class UserMessage(val data: ChatMessage) : ChatListItem()
|
data class UserMessage(val data: ChatMessage) : ChatListItem()
|
||||||
data class AiMessage(val data: ChatMessage, val displayName: String? = null) : ChatListItem()
|
data class AiMessage(val data: ChatMessage, val displayName: String? = null) : ChatListItem()
|
||||||
data class Notice(val text: String) : ChatListItem()
|
data class Notice(val text: String) : ChatListItem()
|
||||||
data class QuotaNotice(val timeText: String? = null) : ChatListItem()
|
object QuotaNotice : ChatListItem()
|
||||||
object TypingIndicator : ChatListItem()
|
object TypingIndicator : ChatListItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ChatQuotaNoticeAction {
|
||||||
|
REWARDED_AD,
|
||||||
|
PURCHASE_10_CAN,
|
||||||
|
PURCHASE_20_CAN
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
// 타이핑 인디케이터 표시용 정보(캐릭터 이름/프로필)
|
// 타이핑 인디케이터 표시용 정보(캐릭터 이름/프로필)
|
||||||
@@ -65,6 +70,9 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
fun onPurchaseMessage(message: ChatMessage)
|
fun onPurchaseMessage(message: ChatMessage)
|
||||||
fun onOpenPurchasedImage(message: ChatMessage)
|
fun onOpenPurchasedImage(message: ChatMessage)
|
||||||
fun onPurchaseQuota()
|
fun onPurchaseQuota()
|
||||||
|
fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||||
|
onPurchaseQuota()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var callback: Callback? = null
|
private var callback: Callback? = null
|
||||||
@@ -79,7 +87,6 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
const val VIEW_TYPE_NOTICE = 3
|
const val VIEW_TYPE_NOTICE = 3
|
||||||
const val VIEW_TYPE_TYPING_INDICATOR = 4
|
const val VIEW_TYPE_TYPING_INDICATOR = 4
|
||||||
const val VIEW_TYPE_QUOTA_NOTICE = 5
|
const val VIEW_TYPE_QUOTA_NOTICE = 5
|
||||||
private const val PAYLOAD_KEY_QUOTA_TIME = "payload_quota_time"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [list]와 [position]을 기준으로 그룹화 여부와 해당 아이템이 그룹의 마지막인지 계산한다.
|
* [list]와 [position]을 기준으로 그룹화 여부와 해당 아이템이 그룹의 마지막인지 계산한다.
|
||||||
@@ -155,16 +162,22 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
return when (item) {
|
return when (item) {
|
||||||
is ChatListItem.UserMessage -> {
|
is ChatListItem.UserMessage -> {
|
||||||
val data = item.data
|
val data = item.data
|
||||||
if (data.messageId != 0L) data.messageId
|
if (data.messageId != 0L) {
|
||||||
else (data.localId?.hashCode()?.toLong()
|
data.messageId
|
||||||
?: ("user:" + data.createdAt + data.message.hashCode()).hashCode().toLong())
|
} else {
|
||||||
|
data.localId?.hashCode()?.toLong()
|
||||||
|
?: ("user:" + data.createdAt + data.message.hashCode()).hashCode().toLong()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChatListItem.AiMessage -> {
|
is ChatListItem.AiMessage -> {
|
||||||
val data = item.data
|
val data = item.data
|
||||||
if (data.messageId != 0L) data.messageId
|
if (data.messageId != 0L) {
|
||||||
else (data.localId?.hashCode()?.toLong()
|
data.messageId
|
||||||
?: ("ai:" + data.createdAt + data.message.hashCode()).hashCode().toLong())
|
} else {
|
||||||
|
data.localId?.hashCode()?.toLong()
|
||||||
|
?: ("ai:" + data.createdAt + data.message.hashCode()).hashCode().toLong()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChatListItem.Notice -> ("notice:" + item.text).hashCode().toLong()
|
is ChatListItem.Notice -> ("notice:" + item.text).hashCode().toLong()
|
||||||
@@ -241,18 +254,6 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
val n = newItems[newItemPosition]
|
val n = newItems[newItemPosition]
|
||||||
return o == n
|
return o == n
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
|
||||||
val o = old[oldItemPosition]
|
|
||||||
val n = newItems[newItemPosition]
|
|
||||||
// QuotaNotice의 timeText만 변경된 경우 부분 갱신 payload 반환
|
|
||||||
if (o is ChatListItem.QuotaNotice && n is ChatListItem.QuotaNotice) {
|
|
||||||
if (o.timeText != n.timeText) {
|
|
||||||
return Bundle().apply { putString(PAYLOAD_KEY_QUOTA_TIME, n.timeText) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(newItems)
|
items.addAll(newItems)
|
||||||
@@ -405,31 +406,13 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is QuotaNoticeViewHolder -> {
|
is QuotaNoticeViewHolder -> {
|
||||||
val item = currItem as ChatListItem.QuotaNotice
|
holder.bind { action ->
|
||||||
holder.bind(item.timeText) {
|
callback?.onQuotaNoticeAction(action)
|
||||||
callback?.onPurchaseQuota()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
if (payloads.isNotEmpty()) {
|
|
||||||
if (holder is QuotaNoticeViewHolder) {
|
|
||||||
val bundle = payloads.find { it is Bundle } as? Bundle
|
|
||||||
if (bundle?.containsKey(PAYLOAD_KEY_QUOTA_TIME) == true) {
|
|
||||||
holder.updateTimeText(bundle.getString(PAYLOAD_KEY_QUOTA_TIME))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onBindViewHolder(holder, position, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
// region ViewHolders
|
// region ViewHolders
|
||||||
|
|
||||||
/** 사용자 메시지 뷰홀더: 시간 포맷팅, 상태(투명도) 표시 */
|
/** 사용자 메시지 뷰홀더: 시간 포맷팅, 상태(투명도) 표시 */
|
||||||
@@ -672,25 +655,16 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
/** 쿼터 안내 메시지 뷰홀더: 제목/남은시간 + 구매 버튼 */
|
/** 쿼터 안내 메시지 뷰홀더: 광고 보기 + 캔 구매 버튼 */
|
||||||
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
private val tvTime: TextView = itemView.findViewById(R.id.tv_time)
|
private val btnRewardedAd: View = itemView.findViewById(R.id.ll_rewarded_ad)
|
||||||
private val btnPurchase: View = itemView.findViewById(R.id.ll_purchase)
|
private val btnPurchase10Can: View = itemView.findViewById(R.id.ll_purchase_10_can)
|
||||||
|
private val btnPurchase20Can: View = itemView.findViewById(R.id.ll_purchase_20_can)
|
||||||
|
|
||||||
fun bind(timeText: String?, onPurchase: () -> Unit) {
|
fun bind(onAction: (ChatQuotaNoticeAction) -> Unit) {
|
||||||
updateTimeText(timeText)
|
btnRewardedAd.setOnClickListener { onAction(ChatQuotaNoticeAction.REWARDED_AD) }
|
||||||
btnPurchase.setOnClickListener { onPurchase() }
|
btnPurchase10Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_10_CAN) }
|
||||||
}
|
btnPurchase20Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_20_CAN) }
|
||||||
|
|
||||||
fun updateTimeText(timeText: String?) {
|
|
||||||
if (timeText.isNullOrBlank()) {
|
|
||||||
if (tvTime.visibility != View.GONE) tvTime.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
if (tvTime.visibility != View.VISIBLE) tvTime.visibility = View.VISIBLE
|
|
||||||
if (tvTime.text?.toString() != timeText) {
|
|
||||||
tvTime.text = timeText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
|||||||
import kr.co.vividnext.sodalive.chat.talk.TalkApi
|
import kr.co.vividnext.sodalive.chat.talk.TalkApi
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao
|
import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatQuotaPurchaseRequest
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatQuotaPurchaseRequest
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaCanOption
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaChargeType
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatQuotaStatusResponse
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatQuotaStatusResponse
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
@@ -73,10 +75,18 @@ class ChatRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 쿼터 구매 */
|
/** 쿼터 구매 */
|
||||||
fun purchaseChatQuota(roomId: Long, token: String): Single<ChatQuotaStatusResponse> {
|
fun purchaseChatQuota(
|
||||||
|
roomId: Long,
|
||||||
|
token: String,
|
||||||
|
chargeType: ChatRoomQuotaChargeType,
|
||||||
|
canOption: ChatRoomQuotaCanOption? = null
|
||||||
|
): Single<ChatQuotaStatusResponse> {
|
||||||
return talkApi.purchaseChatQuota(
|
return talkApi.purchaseChatQuota(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
request = ChatQuotaPurchaseRequest(),
|
request = ChatQuotaPurchaseRequest(
|
||||||
|
chargeType = chargeType,
|
||||||
|
canOption = canOption
|
||||||
|
),
|
||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -87,12 +97,14 @@ class ChatRepository(
|
|||||||
* 로컬에서 최근 20개 메시지 조회
|
* 로컬에서 최근 20개 메시지 조회
|
||||||
*/
|
*/
|
||||||
fun getRecentMessagesFromLocal(roomId: Long): Single<List<ChatMessage>> {
|
fun getRecentMessagesFromLocal(roomId: Long): Single<List<ChatMessage>> {
|
||||||
return Single.fromCallable(Callable {
|
return Single.fromCallable(
|
||||||
runCatching {
|
Callable {
|
||||||
val entities = kotlinx.coroutines.runBlocking { chatDao.getRecentMessages(roomId) }
|
runCatching {
|
||||||
entities.map { it.toDomain() }
|
val entities = kotlinx.coroutines.runBlocking { chatDao.getRecentMessages(roomId) }
|
||||||
}.getOrDefault(emptyList())
|
entities.map { it.toDomain() }
|
||||||
}).subscribeOn(Schedulers.io())
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
).subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,21 +14,31 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import coil.load
|
import coil.load
|
||||||
|
import com.yandex.mobile.ads.common.AdError
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestConfiguration
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestError
|
||||||
|
import com.yandex.mobile.ads.common.ImpressionData
|
||||||
|
import com.yandex.mobile.ads.rewarded.Reward
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAd
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdEventListener
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdLoadListener
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdLoader
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
|
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaCanOption
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaChargeType
|
||||||
import kr.co.vividnext.sodalive.user.UserRepository
|
import kr.co.vividnext.sodalive.user.UserRepository
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||||
ActivityChatRoomBinding::inflate
|
ActivityChatRoomBinding::inflate
|
||||||
@@ -48,13 +58,18 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
private var hasMoreMessages: Boolean = true // Repository 연동 시 서버 값으로 갱신 예정
|
private var hasMoreMessages: Boolean = true // Repository 연동 시 서버 값으로 갱신 예정
|
||||||
private var nextCursor: Long? = null // 가장 오래된 메시지의 timestamp 등
|
private var nextCursor: Long? = null // 가장 오래된 메시지의 timestamp 등
|
||||||
|
|
||||||
// 쿼터/카운트다운 상태
|
// 쿼터/광고 상태
|
||||||
private var quotaTimer: android.os.CountDownTimer? = null
|
private var currentTotalRemaining: Int = Int.MAX_VALUE
|
||||||
|
private var chatQuotaRewardedAdLoader: RewardedAdLoader? = null
|
||||||
|
private var chatQuotaRewardedAd: RewardedAd? = null
|
||||||
|
private var hasRewardHandledForCurrentAd: Boolean = false
|
||||||
|
private var isQuotaPurchaseInFlight: Boolean = false
|
||||||
|
private var isChatQuotaRewardedAdShowing: Boolean = false
|
||||||
|
|
||||||
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
||||||
private var characterInfo: CharacterInfo? = null
|
private var characterInfo: CharacterInfo? = null
|
||||||
|
|
||||||
private fun noticePrefKey(roomId: Long) = "chat_notice_hidden_room_${roomId}"
|
private fun noticePrefKey(roomId: Long): String = "chat_notice_hidden_room_$roomId"
|
||||||
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
||||||
private fun setNoticeHidden(hidden: Boolean) {
|
private fun setNoticeHidden(hidden: Boolean) {
|
||||||
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||||
@@ -132,7 +147,9 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// 타입 배지 텍스트 및 배경
|
// 타입 배지 텍스트 및 배경
|
||||||
val (badgeText, badgeBg) = when (info.characterType) {
|
val (badgeText, badgeBg) = when (info.characterType) {
|
||||||
CharacterType.CLONE -> getString(R.string.chat_character_type_clone) to R.drawable.bg_character_status_clone
|
CharacterType.CLONE -> getString(R.string.chat_character_type_clone) to R.drawable.bg_character_status_clone
|
||||||
CharacterType.CHARACTER -> getString(R.string.chat_character_type_character) to R.drawable.bg_character_status_character
|
CharacterType.CHARACTER -> {
|
||||||
|
getString(R.string.chat_character_type_character) to R.drawable.bg_character_status_character
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.tvCharacterTypeBadge.text = badgeText
|
binding.tvCharacterTypeBadge.text = badgeText
|
||||||
binding.tvCharacterTypeBadge.setBackgroundResource(badgeBg)
|
binding.tvCharacterTypeBadge.setBackgroundResource(badgeBg)
|
||||||
@@ -198,7 +215,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPurchaseQuota() {
|
override fun onPurchaseQuota() {
|
||||||
onPurchaseQuotaClicked()
|
onQuotaNoticeAction(ChatQuotaNoticeAction.REWARDED_AD)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||||
|
this@ChatRoomActivity.onQuotaNoticeAction(action)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -381,7 +402,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 응답에 포함된 쿼터 상태로 UI 갱신
|
// 응답에 포함된 쿼터 상태로 UI 갱신
|
||||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
updateQuotaUi(response.totalRemaining)
|
||||||
}, { error ->
|
}, { error ->
|
||||||
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
||||||
chatAdapter.hideTypingIndicator()
|
chatAdapter.hideTypingIndicator()
|
||||||
@@ -461,17 +482,34 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// endregion 6.2 Send flow
|
// endregion 6.2 Send flow
|
||||||
|
|
||||||
// region Quota handling
|
// region Quota handling
|
||||||
private fun onPurchaseQuotaClicked() {
|
private fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||||
|
if (isQuotaPurchaseInFlight) return
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
ChatQuotaNoticeAction.REWARDED_AD -> showChatQuotaRewardedAd()
|
||||||
|
ChatQuotaNoticeAction.PURCHASE_10_CAN -> purchaseChatQuota(ChatRoomQuotaCanOption.CAN_10)
|
||||||
|
ChatQuotaNoticeAction.PURCHASE_20_CAN -> purchaseChatQuota(ChatRoomQuotaCanOption.CAN_20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun purchaseChatQuota(canOption: ChatRoomQuotaCanOption) {
|
||||||
|
if (isQuotaPurchaseInFlight) return
|
||||||
|
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
isQuotaPurchaseInFlight = true
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
chatRepository.purchaseChatQuota(roomId, token)
|
chatRepository.purchaseChatQuota(
|
||||||
|
roomId = roomId,
|
||||||
|
token = token,
|
||||||
|
chargeType = ChatRoomQuotaChargeType.CAN,
|
||||||
|
canOption = canOption
|
||||||
|
)
|
||||||
|
.doFinally { isQuotaPurchaseInFlight = false }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ resp ->
|
.subscribe({ resp ->
|
||||||
// 쿼터 UI 갱신
|
updateQuotaUi(resp.totalRemaining)
|
||||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
|
||||||
|
|
||||||
// 결제 성공 시 로컬 캔 차감(30캔) 및 헤더 배지 즉시 반영
|
val newCan = (SharedPreferenceManager.can - canOption.needCan).coerceAtLeast(0)
|
||||||
val newCan = (SharedPreferenceManager.can - 30).coerceAtLeast(0)
|
|
||||||
SharedPreferenceManager.can = newCan
|
SharedPreferenceManager.can = newCan
|
||||||
binding.tvCanBadge.text = newCan.moneyFormat()
|
binding.tvCanBadge.text = newCan.moneyFormat()
|
||||||
}, { err ->
|
}, { err ->
|
||||||
@@ -480,33 +518,47 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateQuotaUi(nextRechargeAtEpoch: Long?) {
|
private fun purchaseRewardedChatQuota() {
|
||||||
if (nextRechargeAtEpoch != null) {
|
if (isQuotaPurchaseInFlight) return
|
||||||
// 입력창 숨김 및 안내 표시 + 카운트다운 시작
|
|
||||||
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
isQuotaPurchaseInFlight = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
chatRepository.purchaseChatQuota(
|
||||||
|
roomId = roomId,
|
||||||
|
token = token,
|
||||||
|
chargeType = ChatRoomQuotaChargeType.AD
|
||||||
|
)
|
||||||
|
.doFinally { isQuotaPurchaseInFlight = false }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ resp ->
|
||||||
|
updateQuotaUi(resp.totalRemaining)
|
||||||
|
}, { err ->
|
||||||
|
showToast(err.message ?: getString(R.string.chat_quota_purchase_failed))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateQuotaUi(totalRemaining: Int) {
|
||||||
|
currentTotalRemaining = totalRemaining
|
||||||
|
|
||||||
|
if (totalRemaining <= 0) {
|
||||||
binding.inputContainer.isVisible = false
|
binding.inputContainer.isVisible = false
|
||||||
val timeText = formatEpochToHms(nextRechargeAtEpoch)
|
ensureQuotaNoticeShown()
|
||||||
ensureQuotaNoticeShown(timeText)
|
|
||||||
startQuotaCountdown(nextRechargeAtEpoch)
|
|
||||||
} else {
|
} else {
|
||||||
// 입력창 표시 및 안내 제거
|
|
||||||
binding.inputContainer.isVisible = true
|
binding.inputContainer.isVisible = true
|
||||||
stopQuotaCountdown()
|
|
||||||
ensureQuotaNoticeRemoved()
|
ensureQuotaNoticeRemoved()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totalRemaining <= 1) {
|
||||||
|
preloadChatQuotaRewardedAd()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureQuotaNoticeShown(timeText: String?) {
|
private fun ensureQuotaNoticeShown() {
|
||||||
val idx = items.indexOfLast { it is ChatListItem.QuotaNotice }
|
val idx = items.indexOfLast { it is ChatListItem.QuotaNotice }
|
||||||
val newItem = ChatListItem.QuotaNotice(timeText = timeText)
|
if (idx >= 0) return
|
||||||
if (idx >= 0) {
|
appendMessage(ChatListItem.QuotaNotice)
|
||||||
val old = items[idx] as ChatListItem.QuotaNotice
|
|
||||||
// 동일 시간 텍스트면 불필요한 갱신 회피
|
|
||||||
if (old.timeText == newItem.timeText) return
|
|
||||||
items[idx] = newItem
|
|
||||||
chatAdapter.setItems(items)
|
|
||||||
} else {
|
|
||||||
appendMessage(newItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureQuotaNoticeRemoved() {
|
private fun ensureQuotaNoticeRemoved() {
|
||||||
@@ -517,81 +569,104 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startQuotaCountdown(targetEpoch: Long?) {
|
private val chatQuotaRewardedAdLoadListener = object : RewardedAdLoadListener {
|
||||||
stopQuotaCountdown()
|
override fun onAdLoaded(rewardedAd: RewardedAd) {
|
||||||
if (targetEpoch == null) return
|
chatQuotaRewardedAdLoader = null
|
||||||
val targetMs = if (targetEpoch < 1_000_000_000_000L) targetEpoch * 1000 else targetEpoch
|
clearChatQuotaRewardedAd()
|
||||||
val now = System.currentTimeMillis()
|
chatQuotaRewardedAd = rewardedAd
|
||||||
val duration = targetMs - now
|
hasRewardHandledForCurrentAd = false
|
||||||
if (duration <= 0) {
|
}
|
||||||
checkQuotaStatus()
|
|
||||||
|
override fun onAdFailedToLoad(adRequestError: AdRequestError) {
|
||||||
|
chatQuotaRewardedAdLoader = null
|
||||||
|
chatQuotaRewardedAd = null
|
||||||
|
hasRewardHandledForCurrentAd = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chatQuotaRewardedAdEventListener = object : RewardedAdEventListener {
|
||||||
|
override fun onAdShown() {
|
||||||
|
isChatQuotaRewardedAdShowing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdFailedToShow(adError: AdError) {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdDismissed() {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
val rewardHandled = hasRewardHandledForCurrentAd
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
if (!rewardHandled && currentTotalRemaining <= 1) {
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdClicked() = Unit
|
||||||
|
|
||||||
|
override fun onAdImpression(impressionData: ImpressionData?) = Unit
|
||||||
|
|
||||||
|
override fun onRewarded(reward: Reward) {
|
||||||
|
if (hasRewardHandledForCurrentAd) return
|
||||||
|
hasRewardHandledForCurrentAd = true
|
||||||
|
purchaseRewardedChatQuota()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preloadChatQuotaRewardedAd(force: Boolean = false) {
|
||||||
|
if (!force && (chatQuotaRewardedAd != null || chatQuotaRewardedAdLoader != null)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
quotaTimer = object : android.os.CountDownTimer(duration, 1000L) {
|
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
|
||||||
val timeText =
|
|
||||||
formatMillisToHms((millisUntilFinished + DISPLAY_FUDGE_MS).coerceAtLeast(0L))
|
|
||||||
// 안내 갱신
|
|
||||||
ensureQuotaNoticeShown(timeText)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
val adUnitId = BuildConfig.YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID
|
||||||
ensureQuotaNoticeShown(
|
if (adUnitId.isBlank()) return
|
||||||
SodaLiveApplicationHolder.get()
|
|
||||||
.getString(R.string.screen_audio_content_detail_time_default)
|
|
||||||
)
|
|
||||||
checkQuotaStatus()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopQuotaCountdown() {
|
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||||
quotaTimer?.cancel()
|
chatQuotaRewardedAdLoader = RewardedAdLoader(this).apply {
|
||||||
quotaTimer = null
|
setAdLoadListener(chatQuotaRewardedAdLoadListener)
|
||||||
}
|
}
|
||||||
|
chatQuotaRewardedAdLoader?.loadAd(
|
||||||
private fun checkQuotaStatus() {
|
AdRequestConfiguration.Builder(adUnitId).build()
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
|
||||||
compositeDisposable.add(
|
|
||||||
chatRepository.getChatQuotaStatus(roomId, token)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ resp ->
|
|
||||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
|
||||||
}, { /* 무시: 다음 틱에 재시도 가능 */ })
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatEpochToHms(epoch: Long?): String? {
|
private fun showChatQuotaRewardedAd() {
|
||||||
if (epoch == null) return null
|
if (isQuotaPurchaseInFlight || isChatQuotaRewardedAdShowing) return
|
||||||
val ms = if (epoch < 1_000_000_000_000L) epoch * 1000 else epoch
|
|
||||||
val remain = ms - System.currentTimeMillis()
|
val rewardedAd = chatQuotaRewardedAd
|
||||||
val displayMs = (remain + DISPLAY_FUDGE_MS).coerceAtLeast(0L)
|
if (rewardedAd == null) {
|
||||||
return if (displayMs > 0L) {
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
formatMillisToHms(displayMs)
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
} else {
|
return
|
||||||
SodaLiveApplicationHolder.get()
|
}
|
||||||
.getString(R.string.screen_audio_content_detail_time_default)
|
|
||||||
|
hasRewardHandledForCurrentAd = false
|
||||||
|
isChatQuotaRewardedAdShowing = true
|
||||||
|
rewardedAd.setAdEventListener(chatQuotaRewardedAdEventListener)
|
||||||
|
runCatching {
|
||||||
|
rewardedAd.show(this)
|
||||||
|
}.onFailure {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatMillisToHms(ms: Long): String {
|
private fun clearChatQuotaRewardedAd() {
|
||||||
var totalSec = (ms / 1000).coerceAtLeast(0)
|
chatQuotaRewardedAd?.setAdEventListener(null)
|
||||||
val hours = totalSec / 3600
|
chatQuotaRewardedAd = null
|
||||||
totalSec %= 3600
|
hasRewardHandledForCurrentAd = false
|
||||||
val minutes = totalSec / 60
|
isChatQuotaRewardedAdShowing = false
|
||||||
val seconds = totalSec % 60
|
|
||||||
return String.format(
|
|
||||||
locale = Locale.getDefault(),
|
|
||||||
"%02d:%02d:%02d",
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
private fun releaseChatQuotaRewardedAd() {
|
||||||
stopQuotaCountdown()
|
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||||
super.onDestroy()
|
chatQuotaRewardedAdLoader = null
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
}
|
}
|
||||||
// endregion Quota handling
|
// endregion Quota handling
|
||||||
|
|
||||||
@@ -605,11 +680,17 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
.subscribe({ localList ->
|
.subscribe({ localList ->
|
||||||
if (localList.isNotEmpty() && items.isEmpty()) {
|
if (localList.isNotEmpty() && items.isEmpty()) {
|
||||||
val localItems = localList
|
val localItems = localList
|
||||||
.sortedWith(compareBy<ChatMessage> { it.createdAt }.thenBy { it.messageId }
|
.sortedWith(
|
||||||
.thenBy { it.localId ?: "" })
|
compareBy<ChatMessage> { it.createdAt }
|
||||||
|
.thenBy { it.messageId }
|
||||||
|
.thenBy { it.localId ?: "" }
|
||||||
|
)
|
||||||
.map { msg ->
|
.map { msg ->
|
||||||
if (msg.mine) ChatListItem.UserMessage(msg)
|
if (msg.mine) {
|
||||||
else ChatListItem.AiMessage(msg, characterInfo?.name)
|
ChatListItem.UserMessage(msg)
|
||||||
|
} else {
|
||||||
|
ChatListItem.AiMessage(msg, characterInfo?.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(localItems)
|
items.addAll(localItems)
|
||||||
@@ -665,7 +746,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
// 쿼터 UI 갱신
|
// 쿼터 UI 갱신
|
||||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
updateQuotaUi(response.totalRemaining)
|
||||||
|
|
||||||
// 7.3: 오래된 메시지 정리(백그라운드)
|
// 7.3: 오래된 메시지 정리(백그라운드)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
@@ -760,8 +841,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
.filter { !existingIds.contains(it.messageId) }
|
.filter { !existingIds.contains(it.messageId) }
|
||||||
.map { domain ->
|
.map { domain ->
|
||||||
if (domain.mine) ChatListItem.UserMessage(domain)
|
if (domain.mine) {
|
||||||
else ChatListItem.AiMessage(domain, characterInfo?.name)
|
ChatListItem.UserMessage(domain)
|
||||||
|
} else {
|
||||||
|
ChatListItem.AiMessage(domain, characterInfo?.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상단에 추가하면서 스크롤 위치 보정
|
// 상단에 추가하면서 스크롤 위치 보정
|
||||||
@@ -955,8 +1039,12 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
).show(resources.displayMetrics.widthPixels)
|
).show(resources.displayMetrics.widthPixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
releaseChatQuotaRewardedAd()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DISPLAY_FUDGE_MS: Long = 5_000L
|
|
||||||
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
||||||
|
|
||||||
fun newIntent(context: Context, roomId: Long): Intent {
|
fun newIntent(context: Context, roomId: Long): Intent {
|
||||||
|
|||||||
@@ -5,5 +5,22 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
data class ChatQuotaPurchaseRequest(
|
data class ChatQuotaPurchaseRequest(
|
||||||
@SerializedName("container") val container: String = "aos"
|
@SerializedName("container") val container: String = "aos",
|
||||||
|
@SerializedName("chargeType") val chargeType: ChatRoomQuotaChargeType,
|
||||||
|
@SerializedName("canOption") val canOption: ChatRoomQuotaCanOption? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
enum class ChatRoomQuotaChargeType {
|
||||||
|
CAN,
|
||||||
|
AD
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
enum class ChatRoomQuotaCanOption(
|
||||||
|
val needCan: Int,
|
||||||
|
val quota: Int
|
||||||
|
) {
|
||||||
|
CAN_10(10, 15),
|
||||||
|
CAN_20(20, 40)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package kr.co.vividnext.sodalive.common
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class YandexInlineBannerHeaderAdapter(
|
||||||
|
private val adUnitId: String,
|
||||||
|
private val screenWidth: Int
|
||||||
|
) : RecyclerView.Adapter<YandexInlineBannerHeaderAdapter.BannerViewHolder>() {
|
||||||
|
|
||||||
|
private var bannerAdView: BannerAdView? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BannerViewHolder {
|
||||||
|
val horizontalPadding = 24f.dpToPx().toInt()
|
||||||
|
val verticalPadding = 24f.dpToPx().toInt()
|
||||||
|
val container = FrameLayout(parent.context).apply {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
|
||||||
|
}
|
||||||
|
val bannerView = BannerAdView(parent.context).apply {
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
container.addView(bannerView)
|
||||||
|
return BannerViewHolder(container, bannerView).apply {
|
||||||
|
setIsRecyclable(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BannerViewHolder, position: Int) {
|
||||||
|
bannerAdView = holder.bannerView
|
||||||
|
holder.bannerView.post {
|
||||||
|
val density = holder.bannerView.resources.displayMetrics.density
|
||||||
|
val adWidthPixels = holder.bannerView.width.takeIf { it > 0 }
|
||||||
|
?: (screenWidth - 48f.dpToPx().toInt())
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt().coerceAtLeast(1)
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
holder.bannerView.apply {
|
||||||
|
setAdUnitId(adUnitId)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
context,
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
bannerAdView?.destroy()
|
||||||
|
bannerAdView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class BannerViewHolder(
|
||||||
|
container: FrameLayout,
|
||||||
|
val bannerView: BannerAdView
|
||||||
|
) : RecyclerView.ViewHolder(container)
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package kr.co.vividnext.sodalive.common.image
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Bitmap.Config.ARGB_8888
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.renderscript.Allocation
|
||||||
|
import android.renderscript.Element
|
||||||
|
import android.renderscript.RenderScript
|
||||||
|
import android.renderscript.ScriptIntrinsicBlur
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.graphics.applyCanvas
|
||||||
|
import coil.size.Size
|
||||||
|
import coil.transform.Transformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coil 2.x 환경에서 기존 blur 호출부를 유지하기 위한 Android blur transformation이다.
|
||||||
|
*/
|
||||||
|
@RequiresApi(18)
|
||||||
|
class BlurTransformation @JvmOverloads constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val radius: Float = DEFAULT_RADIUS,
|
||||||
|
private val sampling: Float = DEFAULT_SAMPLING
|
||||||
|
) : Transformation {
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(radius in 0.0f..25.0f) { "radius must be in [0, 25]." }
|
||||||
|
require(sampling > 0f) { "sampling must be > 0." }
|
||||||
|
}
|
||||||
|
|
||||||
|
override val cacheKey: String = "${BlurTransformation::class.java.name}-$radius-$sampling"
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
|
||||||
|
val scaledWidth = (input.width / sampling).toInt().coerceAtLeast(1)
|
||||||
|
val scaledHeight = (input.height / sampling).toInt().coerceAtLeast(1)
|
||||||
|
val output = Bitmap.createBitmap(scaledWidth, scaledHeight, input.config ?: ARGB_8888)
|
||||||
|
|
||||||
|
output.applyCanvas {
|
||||||
|
scale(1f / sampling, 1f / sampling)
|
||||||
|
drawBitmap(input, 0f, 0f, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderScript: RenderScript? = null
|
||||||
|
var inputAllocation: Allocation? = null
|
||||||
|
var outputAllocation: Allocation? = null
|
||||||
|
var blurScript: ScriptIntrinsicBlur? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderScript = RenderScript.create(context)
|
||||||
|
inputAllocation = Allocation.createFromBitmap(
|
||||||
|
renderScript,
|
||||||
|
output,
|
||||||
|
Allocation.MipmapControl.MIPMAP_NONE,
|
||||||
|
Allocation.USAGE_SCRIPT
|
||||||
|
)
|
||||||
|
outputAllocation = Allocation.createTyped(renderScript, inputAllocation.type)
|
||||||
|
blurScript = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
|
||||||
|
|
||||||
|
blurScript.setRadius(radius)
|
||||||
|
blurScript.setInput(inputAllocation)
|
||||||
|
blurScript.forEach(outputAllocation)
|
||||||
|
outputAllocation.copyTo(output)
|
||||||
|
} finally {
|
||||||
|
blurScript?.destroy()
|
||||||
|
inputAllocation?.destroy()
|
||||||
|
outputAllocation?.destroy()
|
||||||
|
renderScript?.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val DEFAULT_RADIUS = 10f
|
||||||
|
private const val DEFAULT_SAMPLING = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -176,6 +176,7 @@ import kr.co.vividnext.sodalive.user.UserViewModel
|
|||||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
||||||
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
@@ -297,6 +298,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { TermsViewModel(get()) }
|
viewModel { TermsViewModel(get()) }
|
||||||
viewModel { FindPasswordViewModel(get()) }
|
viewModel { FindPasswordViewModel(get()) }
|
||||||
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
|
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
|
||||||
|
viewModel { MainV2ViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
|
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { MyPageViewModel(get(), get(), get()) }
|
viewModel { MyPageViewModel(get(), get(), get()) }
|
||||||
viewModel { CanStatusViewModel(get()) }
|
viewModel { CanStatusViewModel(get()) }
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.BlurTransformation
|
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
@@ -38,6 +37,7 @@ import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
|
|||||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import android.content.Context
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.transform.BlurTransformation
|
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
|
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
@@ -28,6 +31,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.Cr
|
|||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBinding>(
|
class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBinding>(
|
||||||
ActivityCreatorCommunityAllBinding::inflate
|
ActivityCreatorCommunityAllBinding::inflate
|
||||||
@@ -142,6 +146,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
mediaPlayerManager.stopContent()
|
mediaPlayerManager.stopContent()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -257,10 +262,32 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
|||||||
)
|
)
|
||||||
|
|
||||||
setupRecyclerViews()
|
setupRecyclerViews()
|
||||||
|
setupInlineBanner()
|
||||||
|
|
||||||
switchToListMode(0, fromGridItemClick = false)
|
switchToListMode(0, fromGridItemClick = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
this@CreatorCommunityAllActivity,
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun bindData() {
|
private fun bindData() {
|
||||||
viewModel.toastLiveData.observe(this) {
|
viewModel.toastLiveData.observe(this) {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
|||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainActivity
|
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||||
import kr.co.vividnext.sodalive.audition.AuditionActivity
|
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.chat.character.CharacterAdapter
|
import kr.co.vividnext.sodalive.chat.character.CharacterAdapter
|
||||||
@@ -174,7 +173,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
setupLatestContent()
|
setupLatestContent()
|
||||||
setupContentBanner()
|
setupContentBanner()
|
||||||
setupOriginalSeries()
|
setupOriginalSeries()
|
||||||
setupAudition()
|
|
||||||
setupSeriesDayOfWeek()
|
setupSeriesDayOfWeek()
|
||||||
setupPopularCharacters()
|
setupPopularCharacters()
|
||||||
setupWeelyChart()
|
setupWeelyChart()
|
||||||
@@ -638,32 +636,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAudition() {
|
|
||||||
val layoutParams = binding
|
|
||||||
.ivAudition
|
|
||||||
.layoutParams as LinearLayout.LayoutParams
|
|
||||||
|
|
||||||
val width = screenWidth - 24.dpToPx()
|
|
||||||
val height = width * 120 / 352
|
|
||||||
layoutParams.width = width.toInt()
|
|
||||||
layoutParams.height = height.toInt()
|
|
||||||
|
|
||||||
binding.ivAudition.layoutParams = layoutParams
|
|
||||||
|
|
||||||
binding.ivAudition.setOnClickListener {
|
|
||||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireContext(),
|
|
||||||
AuditionActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupSeriesDayOfWeek() {
|
private fun setupSeriesDayOfWeek() {
|
||||||
seriesDayOfWeekAdapter = HomeSeriesAdapter {
|
seriesDayOfWeekAdapter = HomeSeriesAdapter {
|
||||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
@@ -16,6 +19,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
|
|||||||
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||||
import kr.co.vividnext.sodalive.settings.notification.NotificationReceiveSettingsActivity
|
import kr.co.vividnext.sodalive.settings.notification.NotificationReceiveSettingsActivity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBinding>(
|
class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBinding>(
|
||||||
ActivityPushNotificationListBinding::inflate
|
ActivityPushNotificationListBinding::inflate
|
||||||
@@ -43,9 +47,36 @@ class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBi
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupCategoryList()
|
setupCategoryList()
|
||||||
|
setupInlineBanner()
|
||||||
setupNotificationList()
|
setupNotificationList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
this@PushNotificationListActivity,
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupCategoryList() {
|
private fun setupCategoryList() {
|
||||||
categoryAdapter = HomeContentThemeAdapter("") { selectedCategory ->
|
categoryAdapter = HomeContentThemeAdapter("") { selectedCategory ->
|
||||||
viewModel.selectCategory(selectedCategory)
|
viewModel.selectCategory(selectedCategory)
|
||||||
|
|||||||
@@ -22,9 +22,12 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
import com.zhpan.bannerview.BaseBannerAdapter
|
import com.zhpan.bannerview.BaseBannerAdapter
|
||||||
import com.zhpan.indicator.enums.IndicatorSlideMode
|
import com.zhpan.indicator.enums.IndicatorSlideMode
|
||||||
import com.zhpan.indicator.enums.IndicatorStyle
|
import com.zhpan.indicator.enums.IndicatorStyle
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
@@ -131,6 +134,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,9 +162,31 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
setupRecommendChannel()
|
setupRecommendChannel()
|
||||||
setupLatestFinishedLiveChannel()
|
setupLatestFinishedLiveChannel()
|
||||||
setupLiveReplay()
|
setupLiveReplay()
|
||||||
|
setupLiveTabInlineBanner()
|
||||||
setupLiveReservation()
|
setupLiveReservation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupLiveTabInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderMakeLiveByRole(role: String) {
|
private fun renderMakeLiveByRole(role: String) {
|
||||||
if (role == MemberRole.CREATOR.name) {
|
if (role == MemberRole.CREATOR.name) {
|
||||||
binding.llMakeLive.visibility = View.VISIBLE
|
binding.llMakeLive.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import kr.co.vividnext.sodalive.common.Constants
|
|||||||
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
import kr.co.vividnext.sodalive.extensions.convertDateFormat
|
||||||
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
|
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class LiveReservationCompleteActivity : BaseActivity<ActivityLiveReservationComp
|
|||||||
binding.tvRemainingCan.text = "${response.remainingCan}"
|
binding.tvRemainingCan.text = "${response.remainingCan}"
|
||||||
|
|
||||||
binding.tvGoHome.setOnClickListener {
|
binding.tvGoHome.setOnClickListener {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
private var isSpeakerMute = false
|
private var isSpeakerMute = false
|
||||||
private var isMicrophoneMute = false
|
private var isMicrophoneMute = false
|
||||||
private var isSpeaker = false
|
private var isSpeaker = false
|
||||||
|
private var hasKnownHostAbsence = false
|
||||||
|
|
||||||
private var isCapturePrivacyMuted = false
|
private var isCapturePrivacyMuted = false
|
||||||
private var isScreenRecordingActive = false
|
private var isScreenRecordingActive = false
|
||||||
@@ -2304,13 +2305,26 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
override fun onUserOffline(uid: Int, reason: Int) {
|
override fun onUserOffline(uid: Int, reason: Int) {
|
||||||
super.onUserOffline(uid, reason)
|
super.onUserOffline(uid, reason)
|
||||||
Logger.e("onUserOffline - uid: $uid")
|
Logger.e("onUserOffline - uid: $uid")
|
||||||
if (viewModel.isEqualToHostId(uid)) {
|
|
||||||
|
val offlineAction = resolveLiveRoomOfflineAction(
|
||||||
|
isHostOffline = viewModel.isEqualToHostId(uid),
|
||||||
|
hasKnownHostAbsence = hasKnownHostAbsence
|
||||||
|
)
|
||||||
|
|
||||||
|
if (offlineAction.shouldMarkHostAbsence) {
|
||||||
|
hasKnownHostAbsence = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offlineAction.shouldFinishRoom) {
|
||||||
handler.post {
|
handler.post {
|
||||||
showToast(getString(R.string.screen_live_room_closed))
|
showToast(getString(R.string.screen_live_room_closed))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
viewModel.getRoomInfo(roomId)
|
}
|
||||||
|
|
||||||
|
if (offlineAction.shouldRefreshRoomInfo) {
|
||||||
|
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||||
speakerListAdapter.muteSpeakers.remove(uid)
|
speakerListAdapter.muteSpeakers.remove(uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2669,7 +2683,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
}
|
}
|
||||||
} else if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_LEAVE) {
|
} else if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_LEAVE) {
|
||||||
if (!viewModel.isEqualToHostId(memberId.toInt())) {
|
if (!viewModel.isEqualToHostId(memberId.toInt())) {
|
||||||
viewModel.getRoomInfo(roomId)
|
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4261,6 +4275,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
|
internal data class LiveRoomOfflineAction(
|
||||||
|
val shouldMarkHostAbsence: Boolean,
|
||||||
|
val shouldFinishRoom: Boolean,
|
||||||
|
val shouldRefreshRoomInfo: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun resolveLiveRoomOfflineAction(
|
||||||
|
isHostOffline: Boolean,
|
||||||
|
hasKnownHostAbsence: Boolean
|
||||||
|
): LiveRoomOfflineAction {
|
||||||
|
if (hasKnownHostAbsence) {
|
||||||
|
return LiveRoomOfflineAction(
|
||||||
|
shouldMarkHostAbsence = false,
|
||||||
|
shouldFinishRoom = false,
|
||||||
|
shouldRefreshRoomInfo = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHostOffline) {
|
||||||
|
return LiveRoomOfflineAction(
|
||||||
|
shouldMarkHostAbsence = true,
|
||||||
|
shouldFinishRoom = true,
|
||||||
|
shouldRefreshRoomInfo = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LiveRoomOfflineAction(
|
||||||
|
shouldMarkHostAbsence = false,
|
||||||
|
shouldFinishRoom = false,
|
||||||
|
shouldRefreshRoomInfo = true
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
|
private val ignorableLiveRoomNotFoundMessages = setOf(
|
||||||
|
"라이브 정보가 없습니다.",
|
||||||
|
"해당하는 라이브의 정보가 없습니다.",
|
||||||
|
"Live session information not found.",
|
||||||
|
"該当するライブの情報がありません。"
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun shouldSuppressLiveRoomInfoError(
|
||||||
|
message: String?,
|
||||||
|
suppressRoomNotFoundError: Boolean
|
||||||
|
): Boolean {
|
||||||
|
if (!suppressRoomNotFoundError) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalizedMessage = message?.trim().orEmpty()
|
||||||
|
if (normalizedMessage.isBlank()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedMessage in ignorableLiveRoomNotFoundMessages
|
||||||
|
}
|
||||||
@@ -232,7 +232,12 @@ class LiveRoomViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRoomInfo(roomId: Long, userId: Int = 0, onSuccess: (String) -> Unit = {}) {
|
fun getRoomInfo(
|
||||||
|
roomId: Long,
|
||||||
|
userId: Int = 0,
|
||||||
|
suppressRoomNotFoundError: Boolean = false,
|
||||||
|
onSuccess: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}")
|
repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}")
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -266,6 +271,10 @@ class LiveRoomViewModel(
|
|||||||
onSuccess(nickname)
|
onSuccess(nickname)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (shouldSuppressLiveRoomInfoError(it.message, suppressRoomNotFoundError)) {
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
if (it.message != null) {
|
if (it.message != null) {
|
||||||
_toastLiveData.postValue(it.message)
|
_toastLiveData.postValue(it.message)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.live.room.detail
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -20,6 +19,9 @@ import coil.transform.CircleCropTransformation
|
|||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
@@ -35,6 +37,7 @@ import org.koin.android.ext.android.inject
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class LiveRoomDetailFragment(
|
class LiveRoomDetailFragment(
|
||||||
private val roomId: Long,
|
private val roomId: Long,
|
||||||
@@ -79,11 +82,39 @@ class LiveRoomDetailFragment(
|
|||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
|
||||||
setupAdapter()
|
setupAdapter()
|
||||||
|
setupLiveRoomDetailInlineBanner()
|
||||||
bindData()
|
bindData()
|
||||||
binding.ivClose.setOnClickListener { dismiss() }
|
binding.ivClose.setOnClickListener { dismiss() }
|
||||||
viewModel.getDetail(roomId) { dismiss() }
|
viewModel.getDetail(roomId) { dismiss() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLiveRoomDetailInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val screenWidth = resources.displayMetrics.widthPixels
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
requireContext(),
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupAdapter() {
|
private fun setupAdapter() {
|
||||||
val recyclerView = binding.rvParticipate
|
val recyclerView = binding.rvParticipate
|
||||||
adapter = LiveRoomDetailAdapter {}
|
adapter = LiveRoomDetailAdapter {}
|
||||||
@@ -384,7 +415,7 @@ class LiveRoomDetailFragment(
|
|||||||
viewModel.shareRoomLink(
|
viewModel.shareRoomLink(
|
||||||
response.roomId,
|
response.roomId,
|
||||||
response.isPrivateRoom,
|
response.isPrivateRoom,
|
||||||
response.password,
|
response.password
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
intent.type = "text/plain"
|
intent.type = "text/plain"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ data class GetRoomInfoResponse(
|
|||||||
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
|
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
|
||||||
@SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false,
|
@SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false,
|
||||||
@SerializedName("isChatFrozen") val isChatFrozen: Boolean = false,
|
@SerializedName("isChatFrozen") val isChatFrozen: Boolean = false,
|
||||||
|
@SerializedName("isFreeRoom") val isFreeRoom: Boolean,
|
||||||
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
||||||
@SerializedName("password") val password: String? = null
|
@SerializedName("password") val password: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
|
|||||||
import kr.co.vividnext.sodalive.message.MessageActivity
|
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
||||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class DeepLinkActivity : AppCompatActivity() {
|
class DeepLinkActivity : AppCompatActivity() {
|
||||||
@@ -63,7 +64,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
||||||
}
|
}
|
||||||
@@ -465,7 +466,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
putExtra(Constants.EXTRA_DATA, extras)
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.webkit.URLUtil
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.load
|
import coil.load
|
||||||
@@ -19,7 +20,7 @@ import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
|
|||||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.FunctionButtonHelper
|
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||||
@@ -36,6 +37,8 @@ import kr.co.vividnext.sodalive.mypage.block.BlockMemberActivity
|
|||||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponActivity
|
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
|
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
|
||||||
|
import kr.co.vividnext.sodalive.mypage.function_button.FunctionButtonAdapter
|
||||||
|
import kr.co.vividnext.sodalive.mypage.function_button.FunctionButtonItem
|
||||||
import kr.co.vividnext.sodalive.mypage.point.PointStatusActivity
|
import kr.co.vividnext.sodalive.mypage.point.PointStatusActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
|
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentAdapter
|
import kr.co.vividnext.sodalive.mypage.recent.RecentContentAdapter
|
||||||
@@ -47,15 +50,21 @@ import kr.co.vividnext.sodalive.settings.notice.NoticeActivity
|
|||||||
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
|
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
|
||||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflate) {
|
class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflate) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FUNCTION_BUTTON_SPAN_COUNT = 4
|
||||||
|
}
|
||||||
|
|
||||||
private val viewModel: MyPageViewModel by inject()
|
private val viewModel: MyPageViewModel by inject()
|
||||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||||
|
|
||||||
private lateinit var loadingDialog: LoadingDialog
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
private val functionButtonAdapter = FunctionButtonAdapter()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -187,6 +196,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
binding.llProfileLoginContainer.visibility = View.GONE
|
binding.llProfileLoginContainer.visibility = View.GONE
|
||||||
|
|
||||||
binding.llFunctionButtonGrid.visibility = View.VISIBLE
|
binding.llFunctionButtonGrid.visibility = View.VISIBLE
|
||||||
|
setupFunctionButtonGrid()
|
||||||
|
|
||||||
binding.ivSettings.setOnClickListener {
|
binding.ivSettings.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
@@ -239,84 +249,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnStorage.root,
|
|
||||||
iconRes = R.drawable.ic_my_storage,
|
|
||||||
title = getString(R.string.screen_my_storage)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireContext(),
|
|
||||||
AudioContentBoxActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnBlockList.root,
|
|
||||||
iconRes = R.drawable.ic_my_block,
|
|
||||||
title = getString(R.string.screen_my_block_list)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireContext(),
|
|
||||||
BlockMemberActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnMorningCall.root,
|
|
||||||
iconRes = R.drawable.ic_my_alarm,
|
|
||||||
title = getString(R.string.screen_my_morning_call)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
AlarmListActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnNotice.root,
|
|
||||||
iconRes = R.drawable.ic_my_notice,
|
|
||||||
title = getString(R.string.screen_my_notice)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
NoticeActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnEvent.root,
|
|
||||||
iconRes = R.drawable.ic_my_event,
|
|
||||||
title = getString(R.string.screen_my_event)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
EventActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnCustomerService.root,
|
|
||||||
iconRes = R.drawable.ic_my_service_center,
|
|
||||||
title = getString(R.string.screen_my_customer_service)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
ServiceCenterActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||||
binding.tvMyChannel.visibility = View.VISIBLE
|
binding.tvMyChannel.visibility = View.VISIBLE
|
||||||
binding.tvMyChannel.setOnClickListener {
|
binding.tvMyChannel.setOnClickListener {
|
||||||
@@ -335,6 +267,8 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
} else {
|
} else {
|
||||||
binding.tvMyChannel.visibility = View.GONE
|
binding.tvMyChannel.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFunctionButtons()
|
||||||
} else {
|
} else {
|
||||||
binding.ivSettings.visibility = View.GONE
|
binding.ivSettings.visibility = View.GONE
|
||||||
binding.llFunctionButtonGrid.visibility = View.GONE
|
binding.llFunctionButtonGrid.visibility = View.GONE
|
||||||
@@ -342,19 +276,19 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
binding.rlProfileContainer.visibility = View.GONE
|
binding.rlProfileContainer.visibility = View.GONE
|
||||||
binding.llProfileLoginContainer.visibility = View.VISIBLE
|
binding.llProfileLoginContainer.visibility = View.VISIBLE
|
||||||
binding.llProfileLoginContainer.setOnClickListener {
|
binding.llProfileLoginContainer.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvCanAmount.text =
|
binding.tvCanAmount.text =
|
||||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||||
binding.tvCanAmount.setOnClickListener {
|
binding.tvCanAmount.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvPointAmount.text =
|
binding.tvPointAmount.text =
|
||||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||||
binding.tvPointAmount.setOnClickListener {
|
binding.tvPointAmount.setOnClickListener {
|
||||||
(requireActivity() as MainActivity).showLoginActivity()
|
showLoginActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvChargeCan.visibility = View.INVISIBLE
|
binding.tvChargeCan.visibility = View.INVISIBLE
|
||||||
@@ -380,58 +314,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.myPageLiveData.observe(viewLifecycleOwner) {
|
viewModel.myPageLiveData.observe(viewLifecycleOwner) {
|
||||||
val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
|
|
||||||
|
|
||||||
if (isKoreanUser) {
|
|
||||||
binding.btnIdentityVerification.root.visibility = View.VISIBLE
|
|
||||||
if (it.isAuth) {
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnIdentityVerification.root,
|
|
||||||
iconRes = R.drawable.ic_my_auth,
|
|
||||||
title = getString(R.string.screen_my_identity_verified)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnIdentityVerification.root,
|
|
||||||
iconRes = R.drawable.ic_my_auth,
|
|
||||||
title = getString(R.string.screen_my_identity_verification)
|
|
||||||
) {
|
|
||||||
showAuthDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.btnIdentityVerification.root.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it.isAuth) {
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnCoupon.root,
|
|
||||||
iconRes = R.drawable.ic_my_coupon,
|
|
||||||
title = getString(R.string.screen_my_coupon_register)
|
|
||||||
) {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
CanCouponActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FunctionButtonHelper.setupFunctionButton(
|
|
||||||
buttonView = binding.btnCoupon.root,
|
|
||||||
iconRes = R.drawable.ic_my_coupon,
|
|
||||||
title = getString(R.string.screen_my_coupon_register)
|
|
||||||
) {
|
|
||||||
Toast.makeText(
|
|
||||||
requireContext(),
|
|
||||||
getString(R.string.screen_my_auth_required),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
showAuthDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.ivProfile.load(it.profileUrl) {
|
binding.ivProfile.load(it.profileUrl) {
|
||||||
crossfade(true)
|
crossfade(true)
|
||||||
placeholder(R.drawable.ic_place_holder)
|
placeholder(R.drawable.ic_place_holder)
|
||||||
@@ -441,9 +323,157 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
|
|
||||||
binding.tvCanAmount.text = (it.chargeCan + it.rewardCan).moneyFormat()
|
binding.tvCanAmount.text = (it.chargeCan + it.rewardCan).moneyFormat()
|
||||||
binding.tvPointAmount.text = it.point.moneyFormat()
|
binding.tvPointAmount.text = it.point.moneyFormat()
|
||||||
|
|
||||||
|
updateFunctionButtons(it.isAuth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupFunctionButtonGrid() {
|
||||||
|
binding.rvFunctionButtons.layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
FUNCTION_BUTTON_SPAN_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
if (binding.rvFunctionButtons.itemDecorationCount == 0) {
|
||||||
|
binding.rvFunctionButtons.addItemDecoration(
|
||||||
|
GridSpacingItemDecoration(
|
||||||
|
spanCount = FUNCTION_BUTTON_SPAN_COUNT,
|
||||||
|
spacing = 16f.dpToPx().toInt(),
|
||||||
|
includeEdge = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.rvFunctionButtons.adapter = functionButtonAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFunctionButtons(isAuth: Boolean? = null) {
|
||||||
|
functionButtonAdapter.submitList(buildFunctionButtonItems(isAuth))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFunctionButtonItems(isAuth: Boolean?): List<FunctionButtonItem> {
|
||||||
|
val isKoreanUser = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
|
||||||
|
|
||||||
|
val items = mutableListOf(
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_storage,
|
||||||
|
title = getString(R.string.screen_my_storage)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireContext(),
|
||||||
|
AudioContentBoxActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_block,
|
||||||
|
title = getString(R.string.screen_my_block_list)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireContext(),
|
||||||
|
BlockMemberActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val shouldShowCouponButton = if (isKoreanUser) {
|
||||||
|
isAuth != null
|
||||||
|
} else {
|
||||||
|
SharedPreferenceManager.isAdultContentVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowCouponButton) {
|
||||||
|
items += FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_coupon,
|
||||||
|
title = getString(R.string.screen_my_coupon_register)
|
||||||
|
) {
|
||||||
|
if ((isAuth == true) || !isKoreanUser) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireActivity(),
|
||||||
|
CanCouponActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
getString(R.string.screen_my_auth_required),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
showAuthDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items += listOf(
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_alarm,
|
||||||
|
title = getString(R.string.screen_my_morning_call)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireActivity(),
|
||||||
|
AlarmListActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_notice,
|
||||||
|
title = getString(R.string.screen_my_notice)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireActivity(),
|
||||||
|
NoticeActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_event,
|
||||||
|
title = getString(R.string.screen_my_event)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireActivity(),
|
||||||
|
EventActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_service_center,
|
||||||
|
title = getString(R.string.screen_my_customer_service)
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
requireActivity(),
|
||||||
|
ServiceCenterActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isKoreanUser && isAuth != null) {
|
||||||
|
items += FunctionButtonItem(
|
||||||
|
iconRes = R.drawable.ic_my_auth,
|
||||||
|
title = if (isAuth) {
|
||||||
|
getString(R.string.screen_my_identity_verified)
|
||||||
|
} else {
|
||||||
|
getString(R.string.screen_my_identity_verification)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!isAuth) {
|
||||||
|
showAuthDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
private fun showAuthDialog() {
|
private fun showAuthDialog() {
|
||||||
Auth.auth(requireActivity(), requireContext()) { json ->
|
Auth.auth(requireActivity(), requireContext()) { json ->
|
||||||
val bootpayResponse = Gson().fromJson(
|
val bootpayResponse = Gson().fromJson(
|
||||||
@@ -469,4 +499,11 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoginActivity() {
|
||||||
|
when (val activity = requireActivity()) {
|
||||||
|
is MainActivity -> activity.showLoginActivity()
|
||||||
|
is MainV2Activity -> activity.showLoginActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,9 +417,9 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
intent?.data?.let { handlePayverseDeeplink(it) }
|
intent.data?.let { handlePayverseDeeplink(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUrl(view: WebView, url: String): Boolean {
|
private fun handleUrl(view: WebView, url: String): Boolean {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityCanStatusBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCanStatusBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||||
import kr.co.vividnext.sodalive.mypage.can.status.charge.CanChargeStatusFragment
|
import kr.co.vividnext.sodalive.mypage.can.status.charge.CanChargeStatusFragment
|
||||||
import kr.co.vividnext.sodalive.mypage.can.status.use.CanUseStatusFragment
|
import kr.co.vividnext.sodalive.mypage.can.status.use.CanUseStatusFragment
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
||||||
@@ -137,7 +137,7 @@ class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickBackButton() {
|
private fun onClickBackButton() {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package kr.co.vividnext.sodalive.mypage.function_button
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.common.FunctionButtonHelper
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ItemFunctionButtonBinding
|
||||||
|
|
||||||
|
class FunctionButtonAdapter : RecyclerView.Adapter<FunctionButtonAdapter.ViewHolder>() {
|
||||||
|
private val items = mutableListOf<FunctionButtonItem>()
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
private val binding: ItemFunctionButtonBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: FunctionButtonItem) {
|
||||||
|
FunctionButtonHelper.setupFunctionButton(
|
||||||
|
buttonView = binding.root,
|
||||||
|
iconRes = item.iconRes,
|
||||||
|
title = item.title,
|
||||||
|
clickListener = View.OnClickListener { item.onClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
|
ItemFunctionButtonBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun submitList(newItems: List<FunctionButtonItem>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.mypage.function_button
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
|
||||||
|
data class FunctionButtonItem(
|
||||||
|
@param:DrawableRes val iconRes: Int,
|
||||||
|
val title: String,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
)
|
||||||
@@ -11,9 +11,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityPointStatusBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityPointStatusBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.mypage.point.reward.PointRewardStatusFragment
|
import kr.co.vividnext.sodalive.mypage.point.reward.PointRewardStatusFragment
|
||||||
import kr.co.vividnext.sodalive.mypage.point.use.PointUseStatusFragment
|
import kr.co.vividnext.sodalive.mypage.point.use.PointUseStatusFragment
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
||||||
@@ -120,7 +120,7 @@ class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickBackButton() {
|
private fun onClickBackButton() {
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent = Intent(applicationContext, MainV2Activity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||||
|
import com.yandex.mobile.ads.common.AdRequest
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
@@ -18,6 +21,7 @@ import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
|||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.following.FollowingCreatorAdapter
|
import kr.co.vividnext.sodalive.following.FollowingCreatorAdapter
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationReceiveSettingsBinding>(
|
class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationReceiveSettingsBinding>(
|
||||||
ActivityNotificationReceiveSettingsBinding::inflate
|
ActivityNotificationReceiveSettingsBinding::inflate
|
||||||
@@ -53,6 +57,33 @@ class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationRec
|
|||||||
binding.ivMessage.setOnClickListener { viewModel.toggleMessage() }
|
binding.ivMessage.setOnClickListener { viewModel.toggleMessage() }
|
||||||
|
|
||||||
setupFollowingChannels()
|
setupFollowingChannels()
|
||||||
|
setupInlineBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
binding.yandexInlineBannerView.destroy()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupInlineBanner() {
|
||||||
|
binding.yandexInlineBannerView.post {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
|
||||||
|
val adWidthDp = (adWidthPixels / density).roundToInt()
|
||||||
|
val maxAdHeightDp = 90
|
||||||
|
|
||||||
|
binding.yandexInlineBannerView.apply {
|
||||||
|
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID)
|
||||||
|
setAdSize(
|
||||||
|
BannerAdSize.inlineSize(
|
||||||
|
this@NotificationReceiveSettingsActivity,
|
||||||
|
adWidthDp,
|
||||||
|
maxAdHeightDp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loadAd(AdRequest.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupFollowingChannels() {
|
private fun setupFollowingChannels() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
|
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
|
|
||||||
@SuppressLint("CustomSplashScreen")
|
@SuppressLint("CustomSplashScreen")
|
||||||
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
|
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
|
||||||
@@ -174,7 +174,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
|
|||||||
private fun showMainActivity(extras: Bundle?) {
|
private fun showMainActivity(extras: Bundle?) {
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(applicationContext, MainActivity::class.java).apply {
|
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_DATA, extras)
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.user.login
|
package kr.co.vividnext.sodalive.user.login
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -45,9 +44,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -434,7 +433,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMain() {
|
private fun navigateToMain() {
|
||||||
val nextIntent = Intent(this@LoginActivity, MainActivity::class.java)
|
val nextIntent = Intent(this@LoginActivity, MainV2Activity::class.java)
|
||||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||||
?: if (intent.extras != null) {
|
?: if (intent.extras != null) {
|
||||||
intent.extras
|
intent.extras
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
|||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivitySignupBinding
|
import kr.co.vividnext.sodalive.databinding.ActivitySignupBinding
|
||||||
import kr.co.vividnext.sodalive.main.MainActivity
|
|
||||||
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
|
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@@ -152,7 +152,7 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMain() {
|
private fun navigateToMain() {
|
||||||
val nextIntent = Intent(this@SignUpActivity, MainActivity::class.java)
|
val nextIntent = Intent(this@SignUpActivity, MainV2Activity::class.java)
|
||||||
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
|
||||||
?: if (intent.extras != null) {
|
?: if (intent.extras != null) {
|
||||||
intent.extras
|
intent.extras
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
||||||
|
|
||||||
|
class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||||
|
FragmentV2MainChatBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||||
|
|
||||||
|
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||||
|
FragmentV2MainContentBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
||||||
|
|
||||||
|
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||||
|
FragmentV2MainHomeBinding::inflate
|
||||||
|
)
|
||||||
@@ -0,0 +1,654 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
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
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
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
|
||||||
|
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.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.common.Constants
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||||
|
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
|
||||||
|
import kr.co.vividnext.sodalive.message.MessageActivity
|
||||||
|
import kr.co.vividnext.sodalive.mypage.MyPageFragment
|
||||||
|
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 kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding::inflate) {
|
||||||
|
|
||||||
|
private val viewModel: MainV2ViewModel by inject()
|
||||||
|
|
||||||
|
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 var playerStateJob: Job? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
overrideRootWindowInsets()
|
||||||
|
|
||||||
|
checkPermissions()
|
||||||
|
trackAppLaunchIfNeeded()
|
||||||
|
pushTokenUpdate()
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
updatePidAndGaid()
|
||||||
|
getEventPopup()
|
||||||
|
observePlayerState()
|
||||||
|
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun overrideRootWindowInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
|
||||||
|
val left = max(systemBars.left, ime.left)
|
||||||
|
val top = systemBars.top
|
||||||
|
val right = max(systemBars.right, ime.right)
|
||||||
|
v.setPadding(left, top, right, 0)
|
||||||
|
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
ViewCompat.requestApplyInsets(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
executeDeeplink(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
getMemberInfo()
|
||||||
|
startService(
|
||||||
|
Intent(this, AudioContentPlayService::class.java).apply {
|
||||||
|
action = AudioContentPlayService.MusicAction.INIT.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
playerStateJob?.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
notificationSettingsDialog = NotificationSettingsDialog(
|
||||||
|
this,
|
||||||
|
layoutInflater
|
||||||
|
) { isNotifiedLive, isNotifiedUploadContent, isNotifiedMessage ->
|
||||||
|
viewModel.updateNotificationSettings(
|
||||||
|
isNotifiedLive,
|
||||||
|
isNotifiedUploadContent,
|
||||||
|
isNotifiedMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBottomNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLoginActivity() {
|
||||||
|
if (SharedPreferenceManager.token.isBlank()) {
|
||||||
|
val extras = intent.extras
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, LoginActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_DATA, extras)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openChatTab() {
|
||||||
|
viewModel.clickTab(MainV2Tab.CHAT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBottomNavigation() {
|
||||||
|
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_main_v2_home -> viewModel.clickTab(MainV2Tab.HOME)
|
||||||
|
R.id.menu_main_v2_content -> viewModel.clickTab(MainV2Tab.CONTENT)
|
||||||
|
R.id.menu_main_v2_chat -> viewModel.clickTab(MainV2Tab.CHAT)
|
||||||
|
R.id.menu_main_v2_my -> viewModel.clickTab(MainV2Tab.MY)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.bottomNavigation.apply {
|
||||||
|
itemIconTintList = null
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.currentTab.observe(this) { tab ->
|
||||||
|
val itemId = when (tab) {
|
||||||
|
MainV2Tab.HOME -> R.id.menu_main_v2_home
|
||||||
|
MainV2Tab.CONTENT -> R.id.menu_main_v2_content
|
||||||
|
MainV2Tab.CHAT -> R.id.menu_main_v2_chat
|
||||||
|
MainV2Tab.MY -> R.id.menu_main_v2_my
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.bottomNavigation.selectedItemId != itemId) {
|
||||||
|
binding.bottomNavigation.selectedItemId = itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFragment(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeFragment(currentTab: MainV2Tab) {
|
||||||
|
val tag = currentTab.toString()
|
||||||
|
val fragmentManager = supportFragmentManager
|
||||||
|
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||||
|
|
||||||
|
fragmentManager.primaryNavigationFragment?.let {
|
||||||
|
fragmentTransaction.hide(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fragment = fragmentManager.findFragmentByTag(tag)
|
||||||
|
if (fragment == null) {
|
||||||
|
fragment = when (currentTab) {
|
||||||
|
MainV2Tab.HOME -> HomeMainFragment()
|
||||||
|
MainV2Tab.CONTENT -> ContentMainFragment()
|
||||||
|
MainV2Tab.CHAT -> ChatMainFragment()
|
||||||
|
MainV2Tab.MY -> MyPageFragment()
|
||||||
|
}
|
||||||
|
fragmentTransaction.add(R.id.fl_container, fragment, tag)
|
||||||
|
} else {
|
||||||
|
fragmentTransaction.show(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentTransaction.setPrimaryNavigationFragment(fragment)
|
||||||
|
fragmentTransaction.setReorderingAllowed(true)
|
||||||
|
fragmentTransaction.commitNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observePlayerState() {
|
||||||
|
playerStateJob = lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||||
|
if (isRunning) {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||||
|
} else {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAndVisibleMiniPlayer() {
|
||||||
|
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||||
|
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
|
||||||
|
binding.ivPlayerStop.setOnClickListener {
|
||||||
|
startService(
|
||||||
|
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
|
||||||
|
action = "STOP_SERVICE"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
connectPlayerService()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectPlayerService() {
|
||||||
|
if (mediaController != null || mediaControllerFuture != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||||
|
val sessionToken = SessionToken(applicationContext, componentName)
|
||||||
|
val controllerFuture =
|
||||||
|
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||||
|
mediaControllerFuture = controllerFuture
|
||||||
|
controllerFuture.addListener(
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (mediaController != null) {
|
||||||
|
controllerFuture.get().release()
|
||||||
|
return@addListener
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController = controllerFuture.get()
|
||||||
|
setupMediaController()
|
||||||
|
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||||
|
|
||||||
|
binding.ivPlayerPlayOrPause.setImageResource(
|
||||||
|
if (mediaController?.isPlaying == true) {
|
||||||
|
R.drawable.ic_player_pause
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMediaMetadata(metadata: MediaMetadata?) {
|
||||||
|
metadata?.let {
|
||||||
|
binding.tvPlayerTitle.text = it.title
|
||||||
|
binding.tvPlayerNickname.text = it.artist
|
||||||
|
|
||||||
|
binding.ivPlayerCover.load(it.artworkUri) {
|
||||||
|
crossfade(true)
|
||||||
|
placeholder(R.drawable.ic_place_holder)
|
||||||
|
transformations(RoundedCornersTransformation(4f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupMediaController() {
|
||||||
|
if (mediaController == null) {
|
||||||
|
deInitMiniPlayer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController!!.addListener(object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
updateMediaMetadata(mediaItem?.mediaMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
binding.ivPlayerPlayOrPause.setImageResource(
|
||||||
|
if (playWhenReady) {
|
||||||
|
R.drawable.ic_player_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_player_play
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deInitMiniPlayer() {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
binding.clMiniPlayer.visibility = View.GONE
|
||||||
|
mediaControllerFuture?.cancel(true)
|
||||||
|
mediaControllerFuture = null
|
||||||
|
mediaController?.release()
|
||||||
|
mediaController = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPlayerFragment() {
|
||||||
|
val playerFragment = AudioContentPlayerFragment(screenWidth, arrayListOf())
|
||||||
|
playerFragment.show(supportFragmentManager, playerFragment.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissions() {
|
||||||
|
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
TedPermission.create()
|
||||||
|
.setPermissionListener(object : PermissionListener {
|
||||||
|
override fun onPermissionGranted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setDeniedMessage(R.string.record_audio_permission_denied_message)
|
||||||
|
.setPermissions(*permissions.toTypedArray())
|
||||||
|
.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trackAppLaunchIfNeeded() {
|
||||||
|
handler.postDelayed({
|
||||||
|
val alreadyTrackingAppLaunch = SharedPreferenceManager.alreadyTrackingAppLaunch
|
||||||
|
val pid = SharedPreferenceManager.marketingPid
|
||||||
|
|
||||||
|
if (!alreadyTrackingAppLaunch && pid.isNotBlank()) {
|
||||||
|
SharedPreferenceManager.alreadyTrackingAppLaunch = true
|
||||||
|
viewModel.adTrackingAppLaunch(pid = pid)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pushTokenUpdate() {
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener {
|
||||||
|
if (!it.isSuccessful) {
|
||||||
|
Logger.v("Fetching FCM registration token failed", it.exception)
|
||||||
|
return@addOnCompleteListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val pushToken = it.result
|
||||||
|
if (pushToken != null) {
|
||||||
|
SharedPreferenceManager.pushToken = pushToken
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
viewModel.pushTokenUpdate(pushToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePidAndGaid() {
|
||||||
|
handler.postDelayed({
|
||||||
|
viewModel.fetchAndUpdateGaidAndPid(context = applicationContext)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMemberInfo() {
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
viewModel.getMemberInfo(context = applicationContext) {
|
||||||
|
notificationSettingsDialog.show(screenWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEventPopup() {
|
||||||
|
viewModel.getEventPopup {
|
||||||
|
if (SharedPreferenceManager.notShowingEventPopupId != it.id) {
|
||||||
|
EventPopupDialogFragment(
|
||||||
|
screenWidth = screenWidth,
|
||||||
|
eventItem = it
|
||||||
|
) {
|
||||||
|
startActivity(
|
||||||
|
Intent(applicationContext, EventDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_EVENT, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.show(supportFragmentManager, EventPopupDialogFragment::class.java.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeDeeplink(intent: Intent) {
|
||||||
|
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return
|
||||||
|
val deepLinkUrl = bundle.getString("deep_link")
|
||||||
|
val routeBundle = if (!deepLinkUrl.isNullOrBlank()) {
|
||||||
|
buildBundleFromDeepLinkUrl(deepLinkUrl) ?: bundle
|
||||||
|
} else {
|
||||||
|
bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (executeBundleRoute(routeBundle)) {
|
||||||
|
clearDeferredDeepLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("channel_id")
|
||||||
|
putQuery("message_id")
|
||||||
|
putQuery("audition_id")
|
||||||
|
putQuery("content_id")
|
||||||
|
putQuery("deep_link_value")
|
||||||
|
putQuery("deep_link_sub5")
|
||||||
|
putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID)
|
||||||
|
putQuery(Constants.EXTRA_COMMUNITY_POST_ID)
|
||||||
|
|
||||||
|
applyPathDeepLink(data = data) { key, value ->
|
||||||
|
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
|
||||||
|
extras.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extras
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyPathDeepLink(
|
||||||
|
data: android.net.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) {
|
||||||
|
"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 executeBundleRoute(bundle: Bundle): Boolean {
|
||||||
|
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 {
|
||||||
|
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 !deepLinkValue.isNullOrBlank() && routeByDeepLinkValue(deepLinkValue, deepLinkValueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
"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 = ""
|
||||||
|
SharedPreferenceManager.marketingUtmCampaign = ""
|
||||||
|
SharedPreferenceManager.marketingLinkValue = ""
|
||||||
|
SharedPreferenceManager.marketingLinkValueId = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLoggedIn(): Boolean {
|
||||||
|
return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
enum class MainV2Tab {
|
||||||
|
HOME,
|
||||||
|
CONTENT,
|
||||||
|
CHAT,
|
||||||
|
MY
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.AddAllPlaybackTrackingRequest
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingData
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
|
||||||
|
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
|
||||||
|
import kr.co.vividnext.sodalive.settings.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||||
|
import kr.co.vividnext.sodalive.settings.event.EventRepository
|
||||||
|
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
|
||||||
|
import kr.co.vividnext.sodalive.tracking.AdTrackingRepository
|
||||||
|
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
|
||||||
|
import kr.co.vividnext.sodalive.tracking.NotiflyClient
|
||||||
|
import kr.co.vividnext.sodalive.user.UserRepository
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class MainV2ViewModel(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val eventRepository: EventRepository,
|
||||||
|
private val adTrackingRepository: AdTrackingRepository,
|
||||||
|
private val audioContentRepository: AudioContentRepository,
|
||||||
|
private val playbackTrackingRepository: PlaybackTrackingRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val _currentTab = MutableLiveData(MainV2Tab.HOME)
|
||||||
|
val currentTab: LiveData<MainV2Tab>
|
||||||
|
get() = _currentTab
|
||||||
|
|
||||||
|
fun clickTab(tab: MainV2Tab) {
|
||||||
|
if (_currentTab.value != tab) {
|
||||||
|
_currentTab.postValue(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNotificationSettings(
|
||||||
|
isNotifiedLive: Boolean,
|
||||||
|
isNotifiedUploadContent: Boolean,
|
||||||
|
isNotifiedMessage: Boolean
|
||||||
|
) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.updateNotificationSettings(
|
||||||
|
request = UpdateNotificationSettingRequest(
|
||||||
|
isNotifiedLive,
|
||||||
|
isNotifiedUploadContent,
|
||||||
|
isNotifiedMessage
|
||||||
|
),
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pushTokenUpdate(pushToken: String) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository
|
||||||
|
.updatePushToken(
|
||||||
|
PushTokenUpdateRequest(pushToken = pushToken),
|
||||||
|
"Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberInfo(context: Context, showNotificationSettingsDialog: () -> Unit) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
val data = it.data
|
||||||
|
SharedPreferenceManager.can = data.can
|
||||||
|
SharedPreferenceManager.point = data.point
|
||||||
|
SharedPreferenceManager.role = data.role.name
|
||||||
|
SharedPreferenceManager.isAuth = data.isAuth
|
||||||
|
|
||||||
|
val localCountryCode = SharedPreferenceManager.countryCode.ifBlank { "KR" }
|
||||||
|
val resolvedCountryCode = data.countryCode?.ifBlank { "KR" } ?: localCountryCode
|
||||||
|
val resolvedIsAdultContentVisible =
|
||||||
|
data.isAdultContentVisible ?: SharedPreferenceManager.isAdultContentVisible
|
||||||
|
val resolvedContentType =
|
||||||
|
data.contentType
|
||||||
|
?: ContentType.entries.getOrNull(SharedPreferenceManager.contentPreference)
|
||||||
|
?: ContentType.ALL
|
||||||
|
|
||||||
|
SharedPreferenceManager.countryCode = resolvedCountryCode
|
||||||
|
SharedPreferenceManager.isAdultContentVisible = resolvedIsAdultContentVisible
|
||||||
|
SharedPreferenceManager.contentPreference = resolvedContentType.ordinal
|
||||||
|
SharedPreferenceManager.isAuditionNotification =
|
||||||
|
data.auditionNotice ?: false
|
||||||
|
if (
|
||||||
|
data.followingChannelUploadContentNotice == null &&
|
||||||
|
data.followingChannelLiveNotice == null &&
|
||||||
|
data.messageNotice == null
|
||||||
|
) {
|
||||||
|
showNotificationSettingsDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd, HH:mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
val lastActiveDate = dateFormat.format(Date())
|
||||||
|
|
||||||
|
val params = mutableMapOf(
|
||||||
|
"nickname" to SharedPreferenceManager.nickname,
|
||||||
|
"last_active_date" to lastActiveDate,
|
||||||
|
"charge_count" to data.chargeCount,
|
||||||
|
"signup_date" to data.signupDate,
|
||||||
|
"is_auth" to data.isAuth,
|
||||||
|
"gender" to data.gender,
|
||||||
|
"can" to data.can
|
||||||
|
)
|
||||||
|
|
||||||
|
NotiflyClient.setUser(
|
||||||
|
context = context,
|
||||||
|
userId = SharedPreferenceManager.userId,
|
||||||
|
params = params
|
||||||
|
)
|
||||||
|
FirebaseTracking.login("email")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAllPlaybackTracking() {
|
||||||
|
val trackingDataList = playbackTrackingRepository.getAllPlaybackTracking()
|
||||||
|
.filter { it.endPosition != null }
|
||||||
|
.filter { it.endPosition!! - it.startPosition >= 4000 }
|
||||||
|
.map {
|
||||||
|
PlaybackTrackingData(it.contentId, it.playDateTime, it.isPreview)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackingDataList.isNotEmpty()) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
audioContentRepository.addAllPlaybackTracking(
|
||||||
|
request = AddAllPlaybackTrackingRequest(trackingDataList = trackingDataList),
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success) {
|
||||||
|
playbackTrackingRepository.removeAllPlaybackTracking()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEventPopup(onSuccess: (EventItem) -> Unit) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
eventRepository.getEventPopup(token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
onSuccess(it.data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchAndUpdateGaidAndPid(context: Context) {
|
||||||
|
Executors.newSingleThreadExecutor().execute {
|
||||||
|
try {
|
||||||
|
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
|
||||||
|
val request = MarketingInfoUpdateRequest(
|
||||||
|
adid = adInfo.id.orEmpty(),
|
||||||
|
pid = SharedPreferenceManager.marketingPid
|
||||||
|
)
|
||||||
|
updateMarketingInfo(request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
updateMarketingInfo(
|
||||||
|
MarketingInfoUpdateRequest(
|
||||||
|
adid = "",
|
||||||
|
pid = SharedPreferenceManager.marketingPid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adTrackingAppLaunch(pid: String) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
adTrackingRepository.appLaunch(pid)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMarketingInfo(request: MarketingInfoUpdateRequest) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
userRepository.updateMarketingInfo(
|
||||||
|
request,
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({}, {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
|
||||||
|
class CapsuleTabBarView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private var container: LinearLayout? = null
|
||||||
|
private var selectionState: CapsuleTabSelectionState? = null
|
||||||
|
private var onTabSelected: ((index: Int) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
container = findViewById(R.id.ll_capsule_tab_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMenus(
|
||||||
|
menus: List<String>,
|
||||||
|
selectedIndex: Int = 0
|
||||||
|
) {
|
||||||
|
applyState(CapsuleTabSelectionState.create(menus, selectedIndex), notifySelection = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectTab(index: Int) {
|
||||||
|
val currentState = selectionState ?: return
|
||||||
|
if (index !in currentState.menus.indices) return
|
||||||
|
if (index == currentState.selectedIndex) return
|
||||||
|
|
||||||
|
applyState(currentState.select(index), notifySelection = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnTabSelectedListener(listener: ((index: Int) -> Unit)?) {
|
||||||
|
onTabSelected = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyState(
|
||||||
|
state: CapsuleTabSelectionState,
|
||||||
|
notifySelection: Boolean
|
||||||
|
) {
|
||||||
|
selectionState = state
|
||||||
|
val tabContainer = requireNotNull(container) { "Capsule tab container is not inflated." }
|
||||||
|
tabContainer.removeAllViews()
|
||||||
|
|
||||||
|
state.menus.forEachIndexed { index, label ->
|
||||||
|
tabContainer.addView(createTabView(label, state.isSelected(index), index))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifySelection) {
|
||||||
|
onTabSelected?.invoke(state.selectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTabView(
|
||||||
|
label: String,
|
||||||
|
selected: Boolean,
|
||||||
|
index: Int
|
||||||
|
): TextView {
|
||||||
|
return TextView(context).apply {
|
||||||
|
text = label
|
||||||
|
isSelected = selected
|
||||||
|
setTextAppearance(R.style.Typography_Body5)
|
||||||
|
setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||||
|
background = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
if (selected) R.drawable.bg_capsule_tab_selected else R.drawable.bg_capsule_tab_normal
|
||||||
|
)
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
minHeight = resources.getDimensionPixelSize(R.dimen.spacing_32)
|
||||||
|
setPadding(
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_12),
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_8),
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_12),
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_8)
|
||||||
|
)
|
||||||
|
setOnClickListener { selectTab(index) }
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_32)
|
||||||
|
).apply {
|
||||||
|
if (index > 0) {
|
||||||
|
marginStart = resources.getDimensionPixelSize(R.dimen.spacing_8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget
|
||||||
|
|
||||||
|
class CapsuleTabSelectionState private constructor(
|
||||||
|
val menus: List<String>,
|
||||||
|
val selectedIndex: Int
|
||||||
|
) {
|
||||||
|
fun isSelected(index: Int): Boolean = selectedIndex == index
|
||||||
|
|
||||||
|
fun select(index: Int): CapsuleTabSelectionState = create(menus, index)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
menus: List<String>,
|
||||||
|
selectedIndex: Int = 0
|
||||||
|
): CapsuleTabSelectionState {
|
||||||
|
require(menus.isNotEmpty()) { "Capsule tab bar requires at least one menu." }
|
||||||
|
|
||||||
|
val normalizedSelectedIndex = if (selectedIndex in menus.indices) selectedIndex else 0
|
||||||
|
return CapsuleTabSelectionState(
|
||||||
|
menus = menus.toList(),
|
||||||
|
selectedIndex = normalizedSelectedIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
|
||||||
|
class TextTabBarView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val tabViews: List<TextView>
|
||||||
|
get() = listOf(
|
||||||
|
findViewById(R.id.tv_text_tab_first),
|
||||||
|
findViewById(R.id.tv_text_tab_second),
|
||||||
|
findViewById(R.id.tv_text_tab_third)
|
||||||
|
)
|
||||||
|
|
||||||
|
private var selectionState: TextTabSelectionState? = null
|
||||||
|
private var onTabSelected: ((index: Int) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
tabViews.forEachIndexed { index, textView ->
|
||||||
|
textView.setOnClickListener { selectTab(index) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMenus(
|
||||||
|
menus: List<String>,
|
||||||
|
selectedIndex: Int = 0
|
||||||
|
) {
|
||||||
|
applyState(TextTabSelectionState.create(menus, selectedIndex), notifySelection = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectTab(index: Int) {
|
||||||
|
val currentState = selectionState ?: return
|
||||||
|
if (index !in currentState.menus.indices) return
|
||||||
|
if (index == currentState.selectedIndex) return
|
||||||
|
|
||||||
|
applyState(currentState.select(index), notifySelection = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnTabSelectedListener(listener: ((index: Int) -> Unit)?) {
|
||||||
|
onTabSelected = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyState(
|
||||||
|
state: TextTabSelectionState,
|
||||||
|
notifySelection: Boolean
|
||||||
|
) {
|
||||||
|
selectionState = state
|
||||||
|
tabViews.forEachIndexed { index, textView ->
|
||||||
|
val hasMenu = index in state.menus.indices
|
||||||
|
textView.visibility = if (hasMenu) View.VISIBLE else View.GONE
|
||||||
|
textView.isSelected = hasMenu && state.isSelected(index)
|
||||||
|
if (hasMenu) {
|
||||||
|
textView.text = state.menus[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifySelection) {
|
||||||
|
onTabSelected?.invoke(state.selectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.widget
|
||||||
|
|
||||||
|
class TextTabSelectionState private constructor(
|
||||||
|
val menus: List<String>,
|
||||||
|
val selectedIndex: Int
|
||||||
|
) {
|
||||||
|
fun isSelected(index: Int): Boolean = selectedIndex == index
|
||||||
|
|
||||||
|
fun select(index: Int): TextTabSelectionState = create(menus, index)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MENU_COUNT = 3
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
menus: List<String>,
|
||||||
|
selectedIndex: Int = 0
|
||||||
|
): TextTabSelectionState {
|
||||||
|
require(menus.size in 1..MAX_MENU_COUNT) { "Text tab bar requires one to three menus." }
|
||||||
|
|
||||||
|
val normalizedSelectedIndex = if (selectedIndex in menus.indices) selectedIndex else 0
|
||||||
|
return TextTabSelectionState(
|
||||||
|
menus = menus.toList(),
|
||||||
|
selectedIndex = normalizedSelectedIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/white" android:state_checked="true" />
|
||||||
|
<item android:color="@color/white" android:state_selected="true" />
|
||||||
|
<item android:color="@color/gray_600" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/color/color_text_tab_bar.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/white" android:state_selected="true" />
|
||||||
|
<item android:color="@color/gray_600" />
|
||||||
|
</selector>
|
||||||
BIN
app/src/main/res/drawable-mdpi/ic_bar_bell.png
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
app/src/main/res/drawable-mdpi/ic_bar_cash.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
app/src/main/res/drawable-mdpi/ic_bar_search.png
Normal file
|
After Width: | Height: | Size: 369 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_chat.png
Normal file
|
After Width: | Height: | Size: 602 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_chat_selected.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_content.png
Normal file
|
After Width: | Height: | Size: 537 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_content_selected.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_home.png
Normal file
|
After Width: | Height: | Size: 564 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_home_selected.png
Normal file
|
After Width: | Height: | Size: 408 B |
BIN
app/src/main/res/drawable-mdpi/ic_nav_my.png
Normal file
|
After Width: | Height: | Size: 723 B |
BIN
app/src/main/res/drawable-mdpi/img_text_logo_v2.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
8
app/src/main/res/drawable/bg_capsule_tab_normal.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/black" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/gray_700" />
|
||||||
|
<corners android:radius="100dp" />
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/bg_capsule_tab_selected.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/soda_400" />
|
||||||
|
<corners android:radius="100dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
|
<solid android:color="#FEF8E3" />
|
||||||
|
<corners android:radius="30dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#F7CB50" />
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/ic_nav_chat_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_chat_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_chat" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_nav_content_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_content_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_content" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_nav_home_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_home_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_home" />
|
||||||
|
</selector>
|
||||||
BIN
app/src/main/res/drawable/ic_nav_my_selected.png
Normal file
|
After Width: | Height: | Size: 556 B |
5
app/src/main/res/drawable/ic_nav_my_tab.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_nav_my_selected" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_nav_my" />
|
||||||
|
</selector>
|
||||||
@@ -396,6 +396,14 @@
|
|||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxHeight="90dp"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:layout_marginTop="13.3dp" />
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -80,6 +80,15 @@
|
|||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:background="#909090" />
|
android:background="#909090" />
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:layout_marginTop="13.3dp"
|
||||||
|
android:layout_marginBottom="13.3dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
|||||||
108
app/src/main/res/layout/activity_main_v2.xml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fl_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/cl_mini_player"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/cl_mini_player"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/color_222222"
|
||||||
|
android:paddingHorizontal="13.3dp"
|
||||||
|
android:paddingVertical="10.7dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_cover"
|
||||||
|
android:layout_width="36.7dp"
|
||||||
|
android:layout_height="36.7dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_player_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="10.7dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/color_eeeeee"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/iv_player_play_or_pause"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/iv_player_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/iv_player_cover"
|
||||||
|
tools:text="JFLA 커버곡 Avicii for your self" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_player_nickname"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2.3dp"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:textColor="@color/color_d2d2d2"
|
||||||
|
android:textSize="11sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/tv_player_title"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/tv_player_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tv_player_title"
|
||||||
|
tools:ignore="SmallSp"
|
||||||
|
tools:text="JFLA 커버곡 Avicii for your self" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_play_or_pause"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/iv_player_stop"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/iv_player_stop"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/iv_player_stop"
|
||||||
|
tools:src="@drawable/btn_bar_play" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_player_stop"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_noti_stop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
android:id="@+id/bottom_navigation"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/black"
|
||||||
|
app:itemActiveIndicatorStyle="@null"
|
||||||
|
app:itemIconTint="@null"
|
||||||
|
app:itemTextAppearanceActive="@style/Typography.Caption3"
|
||||||
|
app:itemTextAppearanceActiveBoldEnabled="false"
|
||||||
|
app:itemTextAppearanceInactive="@style/Typography.Caption3"
|
||||||
|
app:itemTextColor="@color/color_main_v2_bottom_navigation_label"
|
||||||
|
app:labelVisibilityMode="labeled"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:menu="@menu/menu_main_v2_bottom_navigation" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -133,6 +133,14 @@
|
|||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:layout_marginTop="26.7dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="13.3dp" />
|
android:paddingHorizontal="13.3dp" />
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
|||||||
@@ -82,6 +82,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<!-- 인기 캐릭터 섹션 -->
|
<!-- 인기 캐릭터 섹션 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_popular_characters"
|
android:id="@+id/ll_popular_characters"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/black"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|||||||
@@ -253,7 +253,8 @@
|
|||||||
android:layout_marginBottom="48dp"
|
android:layout_marginBottom="48dp"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:src="@drawable/img_banner_audition" />
|
android:src="@drawable/img_banner_audition"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_series_day_of_week"
|
android:id="@+id/ll_series_day_of_week"
|
||||||
|
|||||||
@@ -143,11 +143,19 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="48dp"
|
android:layout_marginBottom="24dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="24dp"
|
android:paddingHorizontal="24dp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_replay_live"
|
android:id="@+id/ll_replay_live"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -187,6 +187,14 @@
|
|||||||
app:drawableStartCompat="@drawable/ic_live_detail_bottom" />
|
app:drawableStartCompat="@drawable/ic_live_detail_bottom" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxHeight="90dp"
|
||||||
|
android:layout_marginHorizontal="13.3dp"
|
||||||
|
android:layout_marginTop="13.3dp" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
|
|||||||
@@ -276,86 +276,13 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<!-- First Row -->
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
<LinearLayout
|
android:id="@+id/rv_function_buttons"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:nestedScrollingEnabled="false"
|
||||||
android:baselineAligned="false"
|
tools:itemCount="8"
|
||||||
android:orientation="horizontal">
|
tools:listitem="@layout/item_function_button" />
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_storage"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_block_list"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_coupon"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_morning_call"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Second Row -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:baselineAligned="false"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_notice"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_event"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_customer_service"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/btn_identity_verification"
|
|
||||||
layout="@layout/item_function_button"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -419,6 +346,7 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
android:id="@+id/rv_original"
|
android:id="@+id/rv_original"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:clipToPadding="false"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:listitem="@layout/item_original_work" />
|
tools:listitem="@layout/item_original_work" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -13,11 +13,20 @@
|
|||||||
android:nestedScrollingEnabled="false"
|
android:nestedScrollingEnabled="false"
|
||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rv_series_by_genre"
|
android:id="@+id/rv_series_by_genre"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -13,11 +13,20 @@
|
|||||||
android:nestedScrollingEnabled="false"
|
android:nestedScrollingEnabled="false"
|
||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rv_series_day_of_week"
|
android:id="@+id/rv_series_day_of_week"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -79,6 +79,14 @@
|
|||||||
android:paddingHorizontal="24dp" />
|
android:paddingHorizontal="24dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.yandex.mobile.ads.banner.BannerAdView
|
||||||
|
android:id="@+id/yandex_inline_banner_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
android:maxHeight="90dp" />
|
||||||
|
|
||||||
<!-- 추천 시리즈 섹션 -->
|
<!-- 추천 시리즈 섹션 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_recommend_series"
|
android:id="@+id/ll_recommend_series"
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:fontFamily="@font/regular"
|
android:fontFamily="@font/regular"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
5
app/src/main/res/layout/fragment_v2_main_chat.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
5
app/src/main/res/layout/fragment_v2_main_content.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
5
app/src/main/res/layout/fragment_v2_main_home.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black" />
|
||||||
@@ -1,81 +1,149 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingVertical="20dp">
|
android:paddingVertical="20dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/ll_rewarded_ad"
|
||||||
android:layout_height="120dp"
|
|
||||||
android:background="@drawable/bg_chat_notice_quota"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_icon"
|
|
||||||
android:layout_width="30dp"
|
|
||||||
android:layout_height="30dp"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_time" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_time"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:fontFamily="@font/bold"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
tools:text="--:--:--" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:fontFamily="@font/bold"
|
|
||||||
android:text="@string/chat_quota_notice_message"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/ll_purchase"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:background="@drawable/bg_chat_quota_rewarded_ad_button"
|
||||||
android:background="@drawable/bg_buy_button"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingVertical="12dp">
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_can" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fontFamily="@font/bold"
|
android:fontFamily="@font/bold"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:text="@string/chat_quota_price"
|
android:text="@string/chat_quota_rewarded_ad_label"
|
||||||
android:textColor="#263238"
|
android:textColor="@color/color_37474f"
|
||||||
android:textSize="24sp" />
|
android:textSize="24sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginHorizontal="4dp"
|
||||||
android:fontFamily="@font/bold"
|
android:fontFamily="@font/medium"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:text="@string/chat_quota_purchase_cta"
|
android:text="@string/chat_quota_separator"
|
||||||
android:textColor="#263238"
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_rewarded_ad_chat_count"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
android:textSize="18sp" />
|
android:textSize="18sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_purchase_10_can"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_buy_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_can" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/bold"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_10_can"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_separator"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_10_can_chat_count"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_purchase_20_can"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_buy_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_can" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/bold"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_20_can"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_separator"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/chat_quota_20_can_chat_count"
|
||||||
|
android:textColor="@color/color_37474f"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|||||||
23
app/src/main/res/layout/view_capsule_tab_bar.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/hsv_capsule_tab_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:scrollbars="none">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_capsule_tab_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="@dimen/spacing_20" />
|
||||||
|
</HorizontalScrollView>
|
||||||
|
</kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView>
|
||||||
43
app/src/main/res/layout/view_text_tab_bar.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<kr.co.vividnext.sodalive.v2.widget.TextTabBarView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:paddingHorizontal="@dimen/spacing_20"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_text_tab_first"
|
||||||
|
style="@style/Typography.Heading3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_20"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="@color/color_text_tab_bar"
|
||||||
|
tools:text="추천" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_text_tab_second"
|
||||||
|
style="@style/Typography.Heading3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_20"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="@color/color_text_tab_bar"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="랭킹"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_text_tab_third"
|
||||||
|
style="@style/Typography.Heading3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="@color/color_text_tab_bar"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="팔로잉"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</kr.co.vividnext.sodalive.v2.widget.TextTabBarView>
|
||||||