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
|
||||
`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)`를 따른다.
|
||||
- 사용자 직접 지시가 명확할 경우 사용자 지시가 최우선이다.
|
||||
|
||||
## 커뮤니케이션 규칙
|
||||
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||
|
||||
## 저장소 범위
|
||||
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
||||
- 모든 명령은 저장소 루트에서 실행한다.
|
||||
- 추측하지 말고 근거 파일(`settings.gradle`, `build.gradle`, `app/build.gradle`, 소스 코드)을 읽고 결정한다.
|
||||
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
||||
## CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)
|
||||
These principles override plugin behavior, skill behavior, workflow behavior, and default model behavior unless the user's direct instruction explicitly says otherwise.
|
||||
|
||||
## 빌드 / 린트 / 테스트 명령
|
||||
기본 실행 형태:
|
||||
```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`에는 테스트 소스가 없으므로 계측 테스트 명령은 신규 테스트 추가 시 사용한다.
|
||||
# CLAUDE.md
|
||||
|
||||
### 1) 단일 테스트 실행 (중요)
|
||||
로컬 단위 테스트(`app/src/test`)는 `--tests` 필터를 사용한다.
|
||||
클래스 단위:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.chat.talk.room.ChatRepositoryTest"
|
||||
```
|
||||
메서드 단위:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.chat.talk.room.ChatRepositoryTest.enterChatRoom inserts messages and returns response"
|
||||
```
|
||||
패턴 매칭 예시:
|
||||
```bash
|
||||
./gradlew :app:testDebugUnitTest --tests "*TimeUtilsTest*"
|
||||
```
|
||||
참고:
|
||||
- Kotlin backtick 테스트명은 공백이 포함될 수 있으므로 전체 문자열을 인용한다.
|
||||
- 메서드 매칭이 불안정하면 클래스 단위로 먼저 실행한다.
|
||||
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||
|
||||
### 2) 계측 테스트 클래스/메서드 타깃 실행
|
||||
Gradle 인자 방식:
|
||||
```bash
|
||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest
|
||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**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 대안:
|
||||
```bash
|
||||
adb shell am instrument -w -e class kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod <test_package>/<runner>
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
## 코드 스타일 가이드
|
||||
### 1) 포맷/기본 규칙
|
||||
- `.editorconfig` 기준을 준수한다.
|
||||
- 인덴트: 공백 4칸, 줄바꿈: LF, 최대 라인 길이: 130.
|
||||
- 파일 끝 개행 유지, trailing whitespace 제거.
|
||||
- Kotlin/KTS에서 `import-ordering` ktlint 규칙은 비활성화되어 있으므로 기존 파일 정렬 스타일을 우선 따른다.
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
### 2) import 규칙
|
||||
- 신규 코드에서는 와일드카드 import(`*`)를 기본적으로 지양한다.
|
||||
- 사용하지 않는 import를 남기지 않는다.
|
||||
- import alias(`as`)는 필요한 경우(이름 충돌 회피) 최소 범위로만 사용한다.
|
||||
- 기존 파일에 와일드카드/alias가 있으면 대규모 정렬 리팩터링 없이 주변 스타일에 맞춘다.
|
||||
---
|
||||
|
||||
### 3) 네이밍/레이어
|
||||
- UI: `*Activity`, `*Fragment`, dialog/sheet suffix
|
||||
- 상태/도메인: `*ViewModel` (주로 `BaseViewModel` 상속)
|
||||
- 데이터 계층: `*Repository`, Retrofit `*Api`
|
||||
- DTO: `data class` + `*Request`, `*Response` suffix
|
||||
- 레이어 흐름: `Api` -> `Repository` -> `ViewModel` -> `Activity`/`Fragment`
|
||||
- DI는 `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`의 Koin 구성을 따른다.
|
||||
**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.
|
||||
|
||||
### 4) 타입/계약/에러 처리
|
||||
- nullability와 제네릭 타입을 의미가 바뀌지 않게 유지한다.
|
||||
- 공개 API/스키마/리소스 계약은 요청 없이 변경하지 않는다.
|
||||
- 응답 처리 시 기존 `ApiResponse<T>`와 Rx 타입(`Single`, `Flowable`)을 우선 재사용한다.
|
||||
- 빈 `catch` 블록을 새로 추가하지 않는다.
|
||||
- 예외를 조용히 삼키지 않고 로그/주석/대체 흐름 중 하나를 남긴다.
|
||||
## 실행 원칙 및 계층 사용 정책
|
||||
- 불확실하거나 모호한 경우 추측하지 말고 확인하거나, 가능한 최소 범위의 안전한 조치를 취한다.
|
||||
|
||||
### 5) 테스트 관례
|
||||
- 단위 테스트는 `app/src/test`에 위치하며 클래스명은 `*Test`를 사용한다.
|
||||
- 기본 스택은 JUnit4 + MockK/Mockito다.
|
||||
- 테스트 추가 시 단일 실행 명령 예시도 본 문서에 갱신한다.
|
||||
### 기본 모드: 보수적 실행
|
||||
- 최소 변경
|
||||
- 단순한 구현
|
||||
- 검증 가능한 결과
|
||||
|
||||
### 6) 주석
|
||||
- 의미 단위별로 주석을 작성한다.
|
||||
- 주석은 한 문장으로 간결하게 작성한다.
|
||||
- 주석은 코드의 의도와 구조를 설명한다.
|
||||
- 주석은 코드 변경 시 업데이트를 잊지 않는다.
|
||||
### 확장 모드
|
||||
- 사용자가 명시적으로 요청한 경우에만 사용한다.
|
||||
- 대규모 리팩토링, 브레인스토밍, 다중 에이전트 실행, 병렬 workflow를 허용한다.
|
||||
|
||||
## 커밋 메시지 규칙 (표준 Conventional Commits)
|
||||
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
|
||||
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
|
||||
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
|
||||
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test` 등)를 사용한다.
|
||||
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
|
||||
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
|
||||
### oh-my-openagent 사용 정책
|
||||
- oh-my-openagent는 opencode의 플러그인 기반 실행 오케스트레이션 계층이다.
|
||||
- oh-my-openagent는 의사결정 권한이 아니라 실행 보조 권한만 가진다.
|
||||
- 작은 작업에는 multi-agent 실행이나 과도한 workflow를 사용하지 않는다.
|
||||
- 병렬 실행은 명확한 이득이 있을 때만 사용한다.
|
||||
- 모든 oh-my-openagent 동작은 `CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)`를 따라야 한다.
|
||||
|
||||
### 커밋 메시지 검증 절차
|
||||
- `git commit` 직전/직후 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
|
||||
- 스크립트 결과가 `[FAIL]`이면 메시지를 수정한 뒤 다시 검증한다.
|
||||
### superpowers 사용 정책
|
||||
- superpowers는 선택적 스킬 계층이다.
|
||||
- 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`)는 생성/수정 여부와 관계없이 커밋하지 않는다.
|
||||
- `app/src/debug/google-services.json`, `app/src/release/google-services.json`은 민감 구성으로 취급하고 외부 공유/로그 출력 금지한다.
|
||||
- `app/build.gradle`의 `buildConfigField` 값(토큰/앱키/시크릿 유사 값)은 신규 하드코딩을 추가하지 않는다.
|
||||
## 저장소 범위
|
||||
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
||||
- 모든 명령은 저장소 루트에서 실행한다.
|
||||
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
||||
- 기존 로직 수정이 아닌 신규 `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, 크래시 메시지에 직접 노출하지 않는다.
|
||||
- 네트워크 로깅은 `AppDI.kt` 패턴을 유지한다(디버그만 BODY, 릴리스는 NONE).
|
||||
- 서명/배포 설정(Crashlytics, Google Services, Proguard, signing)은 요청 없이 변경하지 않는다.
|
||||
- `AndroidManifest.xml` 권한은 민감 영역이므로 신규 추가/확장은 사유와 영향도를 확인한 뒤 반영한다.
|
||||
- `applicationId`, `namespace`, OAuth Client ID, 딥링크 호스트는 요청 없이 변경하지 않는다.
|
||||
- 문서/이슈/PR 본문에 비밀값을 남기지 말고 필요 시 마스킹(`***`) 처리한다.
|
||||
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
|
||||
|
||||
@@ -73,6 +73,21 @@ android {
|
||||
minifyEnabled true
|
||||
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', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
@@ -103,6 +118,20 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
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', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
@@ -156,7 +185,7 @@ dependencies {
|
||||
}
|
||||
|
||||
// image library
|
||||
implementation "io.coil-kt:coil:1.4.0"
|
||||
implementation "io.coil-kt:coil:2.7.0"
|
||||
|
||||
// Koin DI
|
||||
implementation "io.insert-koin:koin-android:3.1.3"
|
||||
@@ -232,6 +261,9 @@ dependencies {
|
||||
// Appsflyer
|
||||
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'
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".main.MainActivity" />
|
||||
<activity android:name=".v2.main.MainV2Activity" />
|
||||
<activity android:name=".user.login.LoginActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
||||
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.facebook.FacebookSdk
|
||||
import com.kakao.sdk.common.KakaoSdk
|
||||
import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.yandex.mobile.ads.common.MobileAds
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
@@ -52,6 +53,8 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
||||
setupAppsFlyer()
|
||||
|
||||
setupNotifly()
|
||||
|
||||
setupYandexAd()
|
||||
}
|
||||
|
||||
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) {
|
||||
super.onStart(owner)
|
||||
isAppInForeground = true
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.bumptech.glide.request.transition.Transition
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
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
|
||||
|
||||
class AudioContentPlayService :
|
||||
@@ -471,7 +471,7 @@ class AudioContentPlayService :
|
||||
}
|
||||
|
||||
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_SINGLE_TOP)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
|
||||
@@ -28,6 +28,18 @@ import coil.transform.RoundedCornersTransformation
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
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.audio_content.AudioContentPlayService
|
||||
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 org.koin.android.ext.android.inject
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
|
||||
@@ -88,6 +101,42 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
private lateinit var orderType: OrderType
|
||||
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")
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
@@ -105,6 +154,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.rlPreviewAlert.visibility = View.GONE
|
||||
|
||||
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
hasConsumedAudioContentPlayInterstitialAttempt = false
|
||||
releaseAudioContentPlayInterstitial()
|
||||
setupAudioContentPlayInterstitial()
|
||||
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
||||
}
|
||||
|
||||
@@ -315,9 +367,125 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
|
||||
}
|
||||
|
||||
setupAudioContentDetailInlineBanner()
|
||||
setupAudioContentPlayInterstitial()
|
||||
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() {
|
||||
val recyclerView = binding.rvBuyer
|
||||
contentBuyerAdapter = AudioContentBuyerAdapter()
|
||||
@@ -775,10 +943,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
R.string.screen_audio_content_detail_total_duration_format,
|
||||
response.duration
|
||||
)
|
||||
audioContentStartPlaybackAction = null
|
||||
isAudioContentInterstitialEligible = false
|
||||
|
||||
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
|
||||
!response.existOrdered &&
|
||||
response.price > 0
|
||||
isAudioContentInterstitialEligible = response.price <= 0 || isAlertPreview
|
||||
|
||||
if (
|
||||
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
|
||||
@@ -798,7 +969,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
val playClickAction = View.OnClickListener {
|
||||
val playAudioContentAction: () -> Unit = {
|
||||
startService(
|
||||
Intent(
|
||||
applicationContext,
|
||||
@@ -842,9 +1013,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
)
|
||||
}
|
||||
|
||||
audioContentStartPlaybackAction = playAudioContentAction
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
|
||||
binding.ivPlayOrPause.setOnClickListener(playClickAction)
|
||||
binding.llPreview.setOnClickListener(playClickAction)
|
||||
updateAudioContentPlayOrPauseControls()
|
||||
|
||||
if (!isAlertPreview) {
|
||||
binding.ivSeekForward10.visibility = View.VISIBLE
|
||||
@@ -873,6 +1045,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
}
|
||||
}
|
||||
} else if (response.releaseDate == null) {
|
||||
audioContentStartPlaybackAction = null
|
||||
isAudioContentInterstitialEligible = false
|
||||
binding.llPreviewNo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
@@ -1166,7 +1340,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
} else {
|
||||
contentOrder(audioContent, orderType)
|
||||
}
|
||||
},
|
||||
}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
@@ -1193,6 +1367,12 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
}, 100)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
binding.yandexInlineBannerView.destroy()
|
||||
releaseAudioContentPlayInterstitial()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
inner class AudioContentReceiver : BroadcastReceiver() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -1222,12 +1402,12 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
viewModel.isLoading.value = isLoading == true
|
||||
|
||||
if (this@AudioContentDetailActivity.audioContentId == contentId) {
|
||||
isAudioContentPlaying = isPlaying == true
|
||||
runOnUiThread {
|
||||
if (changeUi != null && changeUi) {
|
||||
if (isPlaying != null && isPlaying) {
|
||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||
binding.llPreview.visibility = View.GONE
|
||||
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_pause)
|
||||
} else {
|
||||
if (isAlertPreview) {
|
||||
binding.ivPlayOrPause.visibility = View.GONE
|
||||
@@ -1235,9 +1415,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
} else {
|
||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||
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.SharedPreferenceManager
|
||||
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
|
||||
|
||||
@UnstableApi
|
||||
@@ -153,7 +153,7 @@ class AudioContentPlayerService : MediaSessionService() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import coil.load
|
||||
import coil.size.Scale
|
||||
import coil.transform.BlurTransformation
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
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.LoadingDialog
|
||||
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.explorer.profile.CreatorFollowNotifyFragment
|
||||
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.LinearLayoutManager
|
||||
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.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
@@ -39,9 +41,36 @@ class SeriesMainByGenreFragment : BaseFragment<FragmentSeriesMainByGenreBinding>
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
setupGenreView()
|
||||
setupInlineBanner()
|
||||
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() {
|
||||
genreAdapter = GenreAdapter { genre ->
|
||||
seriesAdapter.clear()
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.widget.Toast
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
@@ -40,6 +43,7 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
setupDayOfWeekDay()
|
||||
setupInlineBanner()
|
||||
setupSeriesView()
|
||||
|
||||
val dayOfWeeks = listOf(
|
||||
@@ -59,6 +63,32 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
|
||||
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() {
|
||||
val dayOfWeekAdapter = DayOfWeekAdapter(requireContext()) {
|
||||
adapter.clear()
|
||||
|
||||
@@ -12,9 +12,12 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.indicator.enums.IndicatorSlideMode
|
||||
import com.zhpan.indicator.enums.IndicatorStyle
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
@@ -54,9 +57,36 @@ class SeriesMainHomeFragment : BaseFragment<FragmentSeriesMainHomeBinding>(
|
||||
|
||||
setupBanner()
|
||||
setupCompletedSeriesView()
|
||||
setupInlineBanner()
|
||||
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() {
|
||||
val layoutParams = binding
|
||||
.bannerSlider
|
||||
|
||||
@@ -14,9 +14,12 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.indicator.enums.IndicatorSlideMode
|
||||
import com.zhpan.indicator.enums.IndicatorStyle
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
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.splash.SplashActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// 캐릭터 탭 프래그먼트
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -61,16 +65,43 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
|
||||
viewModel.fetchData()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.yandexInlineBannerView.destroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
setupBanner()
|
||||
setupRecentCharactersRecyclerView()
|
||||
setupCharacterTabInlineBanner()
|
||||
setupPopularCharactersRecyclerView()
|
||||
setupNewCharactersRecyclerView()
|
||||
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() {
|
||||
val layoutParams = binding
|
||||
.bannerSlider
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.base.BaseFragment
|
||||
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.LoadingDialog
|
||||
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.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
@@ -36,6 +39,7 @@ class OriginalTabFragment :
|
||||
private val myPageViewModel: MyPageViewModel by inject()
|
||||
|
||||
private lateinit var adapter: OriginalWorkListAdapter
|
||||
private lateinit var bannerAdapter: YandexInlineBannerHeaderAdapter
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
@@ -49,9 +53,19 @@ class OriginalTabFragment :
|
||||
viewModel.loadMore()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
bannerAdapter.destroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
val spanCount = 3
|
||||
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 ->
|
||||
ensureLoginAndAuth {
|
||||
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(
|
||||
GridSpacingItemDecoration(
|
||||
spanCount,
|
||||
spacingPx,
|
||||
true
|
||||
true,
|
||||
headerCount
|
||||
)
|
||||
)
|
||||
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() {
|
||||
|
||||
@@ -5,11 +5,11 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import coil.load
|
||||
import coil.size.Scale
|
||||
import coil.transform.BlurTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityOriginalWorkDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
@@ -4,12 +4,15 @@ import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||
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.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -20,6 +23,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
||||
private val viewModel: TalkTabViewModel by inject()
|
||||
|
||||
private lateinit var adapter: TalkTabAdapter
|
||||
private lateinit var bannerAdapter: YandexInlineBannerHeaderAdapter
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
@@ -34,7 +38,17 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
||||
viewModel.refreshTalkRooms()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
bannerAdapter.destroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val headerCount = 1
|
||||
bannerAdapter = YandexInlineBannerHeaderAdapter(
|
||||
adUnitId = BuildConfig.YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID,
|
||||
screenWidth = screenWidth
|
||||
)
|
||||
adapter = TalkTabAdapter {
|
||||
startActivity(
|
||||
ChatRoomActivity.newIntent(
|
||||
@@ -56,16 +70,28 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
||||
) {
|
||||
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.right = 24f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
when (adjustedPosition) {
|
||||
0 -> {
|
||||
outRect.top = 24f.dpToPx().toInt()
|
||||
outRect.bottom = 12f.dpToPx().toInt()
|
||||
outRect.bottom = if (adjustedPosition == lastItemPosition) {
|
||||
24f.dpToPx().toInt()
|
||||
} else {
|
||||
12f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
lastItemPosition -> {
|
||||
outRect.top = 12f.dpToPx().toInt()
|
||||
outRect.bottom = 24f.dpToPx().toInt()
|
||||
}
|
||||
@@ -81,7 +107,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
||||
recyclerView.apply {
|
||||
val lm = LinearLayoutManager(requireContext())
|
||||
layoutManager = lm
|
||||
adapter = this@TalkTabFragment.adapter
|
||||
adapter = ConcatAdapter(bannerAdapter, this@TalkTabFragment.adapter)
|
||||
|
||||
// 스크롤 로딩 리스너: 끝에 도달하면 다음 페이지 로드
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
@@ -109,7 +135,7 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
|
||||
adapter.submitList(it)
|
||||
} else {
|
||||
adapter.submitList(emptyList())
|
||||
binding.rvTalk.visibility = View.GONE
|
||||
binding.rvTalk.visibility = View.VISIBLE
|
||||
binding.tvEmpty.visibility = View.VISIBLE
|
||||
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.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
@@ -37,10 +36,16 @@ sealed class ChatListItem {
|
||||
data class UserMessage(val data: ChatMessage) : ChatListItem()
|
||||
data class AiMessage(val data: ChatMessage, val displayName: String? = null) : ChatListItem()
|
||||
data class Notice(val text: String) : ChatListItem()
|
||||
data class QuotaNotice(val timeText: String? = null) : ChatListItem()
|
||||
object QuotaNotice : ChatListItem()
|
||||
object TypingIndicator : ChatListItem()
|
||||
}
|
||||
|
||||
enum class ChatQuotaNoticeAction {
|
||||
REWARDED_AD,
|
||||
PURCHASE_10_CAN,
|
||||
PURCHASE_20_CAN
|
||||
}
|
||||
|
||||
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
// 타이핑 인디케이터 표시용 정보(캐릭터 이름/프로필)
|
||||
@@ -65,6 +70,9 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
fun onPurchaseMessage(message: ChatMessage)
|
||||
fun onOpenPurchasedImage(message: ChatMessage)
|
||||
fun onPurchaseQuota()
|
||||
fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||
onPurchaseQuota()
|
||||
}
|
||||
}
|
||||
|
||||
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_TYPING_INDICATOR = 4
|
||||
const val VIEW_TYPE_QUOTA_NOTICE = 5
|
||||
private const val PAYLOAD_KEY_QUOTA_TIME = "payload_quota_time"
|
||||
|
||||
/**
|
||||
* [list]와 [position]을 기준으로 그룹화 여부와 해당 아이템이 그룹의 마지막인지 계산한다.
|
||||
@@ -155,16 +162,22 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
return when (item) {
|
||||
is ChatListItem.UserMessage -> {
|
||||
val data = item.data
|
||||
if (data.messageId != 0L) data.messageId
|
||||
else (data.localId?.hashCode()?.toLong()
|
||||
?: ("user:" + data.createdAt + data.message.hashCode()).hashCode().toLong())
|
||||
if (data.messageId != 0L) {
|
||||
data.messageId
|
||||
} else {
|
||||
data.localId?.hashCode()?.toLong()
|
||||
?: ("user:" + data.createdAt + data.message.hashCode()).hashCode().toLong()
|
||||
}
|
||||
}
|
||||
|
||||
is ChatListItem.AiMessage -> {
|
||||
val data = item.data
|
||||
if (data.messageId != 0L) data.messageId
|
||||
else (data.localId?.hashCode()?.toLong()
|
||||
?: ("ai:" + data.createdAt + data.message.hashCode()).hashCode().toLong())
|
||||
if (data.messageId != 0L) {
|
||||
data.messageId
|
||||
} else {
|
||||
data.localId?.hashCode()?.toLong()
|
||||
?: ("ai:" + data.createdAt + data.message.hashCode()).hashCode().toLong()
|
||||
}
|
||||
}
|
||||
|
||||
is ChatListItem.Notice -> ("notice:" + item.text).hashCode().toLong()
|
||||
@@ -241,18 +254,6 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
val n = newItems[newItemPosition]
|
||||
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.addAll(newItems)
|
||||
@@ -405,31 +406,13 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
}
|
||||
|
||||
is QuotaNoticeViewHolder -> {
|
||||
val item = currItem as ChatListItem.QuotaNotice
|
||||
holder.bind(item.timeText) {
|
||||
callback?.onPurchaseQuota()
|
||||
holder.bind { action ->
|
||||
callback?.onQuotaNoticeAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/** 사용자 메시지 뷰홀더: 시간 포맷팅, 상태(투명도) 표시 */
|
||||
@@ -672,25 +655,16 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
// endregion
|
||||
|
||||
/** 쿼터 안내 메시지 뷰홀더: 제목/남은시간 + 구매 버튼 */
|
||||
/** 쿼터 안내 메시지 뷰홀더: 광고 보기 + 캔 구매 버튼 */
|
||||
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val tvTime: TextView = itemView.findViewById(R.id.tv_time)
|
||||
private val btnPurchase: View = itemView.findViewById(R.id.ll_purchase)
|
||||
private val btnRewardedAd: View = itemView.findViewById(R.id.ll_rewarded_ad)
|
||||
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) {
|
||||
updateTimeText(timeText)
|
||||
btnPurchase.setOnClickListener { onPurchase() }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
fun bind(onAction: (ChatQuotaNoticeAction) -> Unit) {
|
||||
btnRewardedAd.setOnClickListener { onAction(ChatQuotaNoticeAction.REWARDED_AD) }
|
||||
btnPurchase10Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_10_CAN) }
|
||||
btnPurchase20Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_20_CAN) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.room.db.ChatMessageDao
|
||||
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.common.ApiResponse
|
||||
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(
|
||||
roomId = roomId,
|
||||
request = ChatQuotaPurchaseRequest(),
|
||||
request = ChatQuotaPurchaseRequest(
|
||||
chargeType = chargeType,
|
||||
canOption = canOption
|
||||
),
|
||||
authHeader = token
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -87,12 +97,14 @@ class ChatRepository(
|
||||
* 로컬에서 최근 20개 메시지 조회
|
||||
*/
|
||||
fun getRecentMessagesFromLocal(roomId: Long): Single<List<ChatMessage>> {
|
||||
return Single.fromCallable(Callable {
|
||||
runCatching {
|
||||
val entities = kotlinx.coroutines.runBlocking { chatDao.getRecentMessages(roomId) }
|
||||
entities.map { it.toDomain() }
|
||||
}.getOrDefault(emptyList())
|
||||
}).subscribeOn(Schedulers.io())
|
||||
return Single.fromCallable(
|
||||
Callable {
|
||||
runCatching {
|
||||
val entities = kotlinx.coroutines.runBlocking { chatDao.getRecentMessages(roomId) }
|
||||
entities.map { it.toDomain() }
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,21 +14,31 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
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.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
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.extensions.dpToPx
|
||||
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 org.koin.android.ext.android.inject
|
||||
import java.util.Locale
|
||||
|
||||
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
ActivityChatRoomBinding::inflate
|
||||
@@ -48,13 +58,18 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
private var hasMoreMessages: Boolean = true // Repository 연동 시 서버 값으로 갱신 예정
|
||||
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 보관)
|
||||
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 setNoticeHidden(hidden: Boolean) {
|
||||
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||
@@ -132,7 +147,9 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
// 타입 배지 텍스트 및 배경
|
||||
val (badgeText, badgeBg) = when (info.characterType) {
|
||||
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.setBackgroundResource(badgeBg)
|
||||
@@ -198,7 +215,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
}
|
||||
|
||||
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 갱신
|
||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
||||
updateQuotaUi(response.totalRemaining)
|
||||
}, { error ->
|
||||
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
||||
chatAdapter.hideTypingIndicator()
|
||||
@@ -461,17 +482,34 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
// endregion 6.2 Send flow
|
||||
|
||||
// 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}"
|
||||
isQuotaPurchaseInFlight = true
|
||||
compositeDisposable.add(
|
||||
chatRepository.purchaseChatQuota(roomId, token)
|
||||
chatRepository.purchaseChatQuota(
|
||||
roomId = roomId,
|
||||
token = token,
|
||||
chargeType = ChatRoomQuotaChargeType.CAN,
|
||||
canOption = canOption
|
||||
)
|
||||
.doFinally { isQuotaPurchaseInFlight = false }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ resp ->
|
||||
// 쿼터 UI 갱신
|
||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
||||
updateQuotaUi(resp.totalRemaining)
|
||||
|
||||
// 결제 성공 시 로컬 캔 차감(30캔) 및 헤더 배지 즉시 반영
|
||||
val newCan = (SharedPreferenceManager.can - 30).coerceAtLeast(0)
|
||||
val newCan = (SharedPreferenceManager.can - canOption.needCan).coerceAtLeast(0)
|
||||
SharedPreferenceManager.can = newCan
|
||||
binding.tvCanBadge.text = newCan.moneyFormat()
|
||||
}, { err ->
|
||||
@@ -480,33 +518,47 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateQuotaUi(nextRechargeAtEpoch: Long?) {
|
||||
if (nextRechargeAtEpoch != null) {
|
||||
// 입력창 숨김 및 안내 표시 + 카운트다운 시작
|
||||
private fun purchaseRewardedChatQuota() {
|
||||
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
|
||||
val timeText = formatEpochToHms(nextRechargeAtEpoch)
|
||||
ensureQuotaNoticeShown(timeText)
|
||||
startQuotaCountdown(nextRechargeAtEpoch)
|
||||
ensureQuotaNoticeShown()
|
||||
} else {
|
||||
// 입력창 표시 및 안내 제거
|
||||
binding.inputContainer.isVisible = true
|
||||
stopQuotaCountdown()
|
||||
ensureQuotaNoticeRemoved()
|
||||
}
|
||||
|
||||
if (totalRemaining <= 1) {
|
||||
preloadChatQuotaRewardedAd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureQuotaNoticeShown(timeText: String?) {
|
||||
private fun ensureQuotaNoticeShown() {
|
||||
val idx = items.indexOfLast { it is ChatListItem.QuotaNotice }
|
||||
val newItem = ChatListItem.QuotaNotice(timeText = timeText)
|
||||
if (idx >= 0) {
|
||||
val old = items[idx] as ChatListItem.QuotaNotice
|
||||
// 동일 시간 텍스트면 불필요한 갱신 회피
|
||||
if (old.timeText == newItem.timeText) return
|
||||
items[idx] = newItem
|
||||
chatAdapter.setItems(items)
|
||||
} else {
|
||||
appendMessage(newItem)
|
||||
}
|
||||
if (idx >= 0) return
|
||||
appendMessage(ChatListItem.QuotaNotice)
|
||||
}
|
||||
|
||||
private fun ensureQuotaNoticeRemoved() {
|
||||
@@ -517,81 +569,104 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQuotaCountdown(targetEpoch: Long?) {
|
||||
stopQuotaCountdown()
|
||||
if (targetEpoch == null) return
|
||||
val targetMs = if (targetEpoch < 1_000_000_000_000L) targetEpoch * 1000 else targetEpoch
|
||||
val now = System.currentTimeMillis()
|
||||
val duration = targetMs - now
|
||||
if (duration <= 0) {
|
||||
checkQuotaStatus()
|
||||
private val chatQuotaRewardedAdLoadListener = object : RewardedAdLoadListener {
|
||||
override fun onAdLoaded(rewardedAd: RewardedAd) {
|
||||
chatQuotaRewardedAdLoader = null
|
||||
clearChatQuotaRewardedAd()
|
||||
chatQuotaRewardedAd = rewardedAd
|
||||
hasRewardHandledForCurrentAd = false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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() {
|
||||
ensureQuotaNoticeShown(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_audio_content_detail_time_default)
|
||||
)
|
||||
checkQuotaStatus()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
val adUnitId = BuildConfig.YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID
|
||||
if (adUnitId.isBlank()) return
|
||||
|
||||
private fun stopQuotaCountdown() {
|
||||
quotaTimer?.cancel()
|
||||
quotaTimer = null
|
||||
}
|
||||
|
||||
private fun checkQuotaStatus() {
|
||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||
compositeDisposable.add(
|
||||
chatRepository.getChatQuotaStatus(roomId, token)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ resp ->
|
||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
||||
}, { /* 무시: 다음 틱에 재시도 가능 */ })
|
||||
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||
chatQuotaRewardedAdLoader = RewardedAdLoader(this).apply {
|
||||
setAdLoadListener(chatQuotaRewardedAdLoadListener)
|
||||
}
|
||||
chatQuotaRewardedAdLoader?.loadAd(
|
||||
AdRequestConfiguration.Builder(adUnitId).build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatEpochToHms(epoch: Long?): String? {
|
||||
if (epoch == null) return null
|
||||
val ms = if (epoch < 1_000_000_000_000L) epoch * 1000 else epoch
|
||||
val remain = ms - System.currentTimeMillis()
|
||||
val displayMs = (remain + DISPLAY_FUDGE_MS).coerceAtLeast(0L)
|
||||
return if (displayMs > 0L) {
|
||||
formatMillisToHms(displayMs)
|
||||
} else {
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_audio_content_detail_time_default)
|
||||
private fun showChatQuotaRewardedAd() {
|
||||
if (isQuotaPurchaseInFlight || isChatQuotaRewardedAdShowing) return
|
||||
|
||||
val rewardedAd = chatQuotaRewardedAd
|
||||
if (rewardedAd == null) {
|
||||
preloadChatQuotaRewardedAd(force = true)
|
||||
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
var totalSec = (ms / 1000).coerceAtLeast(0)
|
||||
val hours = totalSec / 3600
|
||||
totalSec %= 3600
|
||||
val minutes = totalSec / 60
|
||||
val seconds = totalSec % 60
|
||||
return String.format(
|
||||
locale = Locale.getDefault(),
|
||||
"%02d:%02d:%02d",
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
private fun clearChatQuotaRewardedAd() {
|
||||
chatQuotaRewardedAd?.setAdEventListener(null)
|
||||
chatQuotaRewardedAd = null
|
||||
hasRewardHandledForCurrentAd = false
|
||||
isChatQuotaRewardedAdShowing = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopQuotaCountdown()
|
||||
super.onDestroy()
|
||||
private fun releaseChatQuotaRewardedAd() {
|
||||
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||
chatQuotaRewardedAdLoader = null
|
||||
clearChatQuotaRewardedAd()
|
||||
}
|
||||
// endregion Quota handling
|
||||
|
||||
@@ -605,11 +680,17 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
.subscribe({ localList ->
|
||||
if (localList.isNotEmpty() && items.isEmpty()) {
|
||||
val localItems = localList
|
||||
.sortedWith(compareBy<ChatMessage> { it.createdAt }.thenBy { it.messageId }
|
||||
.thenBy { it.localId ?: "" })
|
||||
.sortedWith(
|
||||
compareBy<ChatMessage> { it.createdAt }
|
||||
.thenBy { it.messageId }
|
||||
.thenBy { it.localId ?: "" }
|
||||
)
|
||||
.map { msg ->
|
||||
if (msg.mine) ChatListItem.UserMessage(msg)
|
||||
else ChatListItem.AiMessage(msg, characterInfo?.name)
|
||||
if (msg.mine) {
|
||||
ChatListItem.UserMessage(msg)
|
||||
} else {
|
||||
ChatListItem.AiMessage(msg, characterInfo?.name)
|
||||
}
|
||||
}
|
||||
items.clear()
|
||||
items.addAll(localItems)
|
||||
@@ -665,7 +746,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
isLoading = false
|
||||
|
||||
// 쿼터 UI 갱신
|
||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
||||
updateQuotaUi(response.totalRemaining)
|
||||
|
||||
// 7.3: 오래된 메시지 정리(백그라운드)
|
||||
compositeDisposable.add(
|
||||
@@ -760,8 +841,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
.map { it.toDomain() }
|
||||
.filter { !existingIds.contains(it.messageId) }
|
||||
.map { domain ->
|
||||
if (domain.mine) ChatListItem.UserMessage(domain)
|
||||
else ChatListItem.AiMessage(domain, characterInfo?.name)
|
||||
if (domain.mine) {
|
||||
ChatListItem.UserMessage(domain)
|
||||
} else {
|
||||
ChatListItem.AiMessage(domain, characterInfo?.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 상단에 추가하면서 스크롤 위치 보정
|
||||
@@ -955,8 +1039,12 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
).show(resources.displayMetrics.widthPixels)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseChatQuotaRewardedAd()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DISPLAY_FUDGE_MS: Long = 5_000L
|
||||
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
||||
|
||||
fun newIntent(context: Context, roomId: Long): Intent {
|
||||
|
||||
@@ -5,5 +5,22 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
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.login.LoginViewModel
|
||||
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
@@ -297,6 +298,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
viewModel { TermsViewModel(get()) }
|
||||
viewModel { FindPasswordViewModel(get()) }
|
||||
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel { MainV2ViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel { MyPageViewModel(get(), get(), get()) }
|
||||
viewModel { CanStatusViewModel(get()) }
|
||||
|
||||
@@ -22,7 +22,6 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.BlurTransformation
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
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.detail.SeriesDetailActivity
|
||||
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.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
|
||||
@@ -4,11 +4,11 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.BlurTransformation
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import coil.transform.Transformation
|
||||
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.extensions.dpToPx
|
||||
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.LinearLayoutManager
|
||||
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.base.BaseActivity
|
||||
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.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBinding>(
|
||||
ActivityCreatorCommunityAllBinding::inflate
|
||||
@@ -142,6 +146,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
binding.yandexInlineBannerView.destroy()
|
||||
mediaPlayerManager.stopContent()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -257,10 +262,32 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
|
||||
)
|
||||
|
||||
setupRecyclerViews()
|
||||
setupInlineBanner()
|
||||
|
||||
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")
|
||||
private fun bindData() {
|
||||
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.main.SeriesMainActivity
|
||||
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.SodaDialog
|
||||
import kr.co.vividnext.sodalive.chat.character.CharacterAdapter
|
||||
@@ -174,7 +173,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
setupLatestContent()
|
||||
setupContentBanner()
|
||||
setupOriginalSeries()
|
||||
setupAudition()
|
||||
setupSeriesDayOfWeek()
|
||||
setupPopularCharacters()
|
||||
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() {
|
||||
seriesDayOfWeekAdapter = HomeSeriesAdapter {
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.base.BaseActivity
|
||||
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.settings.notification.NotificationReceiveSettingsActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBinding>(
|
||||
ActivityPushNotificationListBinding::inflate
|
||||
@@ -43,9 +47,36 @@ class PushNotificationListActivity : BaseActivity<ActivityPushNotificationListBi
|
||||
}
|
||||
|
||||
setupCategoryList()
|
||||
setupInlineBanner()
|
||||
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() {
|
||||
categoryAdapter = HomeContentThemeAdapter("") { selectedCategory ->
|
||||
viewModel.selectCategory(selectedCategory)
|
||||
|
||||
@@ -22,9 +22,12 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.indicator.enums.IndicatorSlideMode
|
||||
import com.zhpan.indicator.enums.IndicatorStyle
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
@@ -131,6 +134,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.yandexInlineBannerView.destroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -158,9 +162,31 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
setupRecommendChannel()
|
||||
setupLatestFinishedLiveChannel()
|
||||
setupLiveReplay()
|
||||
setupLiveTabInlineBanner()
|
||||
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) {
|
||||
if (role == MemberRole.CREATOR.name) {
|
||||
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.extensions.convertDateFormat
|
||||
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.v2.main.MainV2Activity
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@@ -52,7 +52,7 @@ class LiveReservationCompleteActivity : BaseActivity<ActivityLiveReservationComp
|
||||
binding.tvRemainingCan.text = "${response.remainingCan}"
|
||||
|
||||
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_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
|
||||
@@ -170,6 +170,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
private var isSpeakerMute = false
|
||||
private var isMicrophoneMute = false
|
||||
private var isSpeaker = false
|
||||
private var hasKnownHostAbsence = false
|
||||
|
||||
private var isCapturePrivacyMuted = false
|
||||
private var isScreenRecordingActive = false
|
||||
@@ -2304,13 +2305,26 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
override fun onUserOffline(uid: Int, reason: Int) {
|
||||
super.onUserOffline(uid, reason)
|
||||
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 {
|
||||
showToast(getString(R.string.screen_live_room_closed))
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
viewModel.getRoomInfo(roomId)
|
||||
return
|
||||
}
|
||||
|
||||
if (offlineAction.shouldRefreshRoomInfo) {
|
||||
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||
speakerListAdapter.muteSpeakers.remove(uid)
|
||||
}
|
||||
}
|
||||
@@ -2669,7 +2683,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
} else if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_LEAVE) {
|
||||
if (!viewModel.isEqualToHostId(memberId.toInt())) {
|
||||
viewModel.getRoomInfo(roomId)
|
||||
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4261,6 +4275,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
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(
|
||||
repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -266,6 +271,10 @@ class LiveRoomViewModel(
|
||||
onSuccess(nickname)
|
||||
}
|
||||
} else {
|
||||
if (shouldSuppressLiveRoomInfoError(it.message, suppressRoomNotFoundError)) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.live.room.detail
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -20,6 +19,9 @@ import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
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.common.Constants
|
||||
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.TimeZone
|
||||
import androidx.core.net.toUri
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class LiveRoomDetailFragment(
|
||||
private val roomId: Long,
|
||||
@@ -79,11 +82,39 @@ class LiveRoomDetailFragment(
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
setupAdapter()
|
||||
setupLiveRoomDetailInlineBanner()
|
||||
bindData()
|
||||
binding.ivClose.setOnClickListener { 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() {
|
||||
val recyclerView = binding.rvParticipate
|
||||
adapter = LiveRoomDetailAdapter {}
|
||||
@@ -384,7 +415,7 @@ class LiveRoomDetailFragment(
|
||||
viewModel.shareRoomLink(
|
||||
response.roomId,
|
||||
response.isPrivateRoom,
|
||||
response.password,
|
||||
response.password
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
|
||||
@@ -29,6 +29,7 @@ data class GetRoomInfoResponse(
|
||||
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
|
||||
@SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false,
|
||||
@SerializedName("isChatFrozen") val isChatFrozen: Boolean = false,
|
||||
@SerializedName("isFreeRoom") val isFreeRoom: Boolean,
|
||||
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
||||
@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.mypage.can.payment.CanPaymentActivity
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import java.util.Locale
|
||||
|
||||
class DeepLinkActivity : AppCompatActivity() {
|
||||
@@ -63,7 +64,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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)
|
||||
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
|
||||
}
|
||||
@@ -465,7 +466,7 @@ class DeepLinkActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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)
|
||||
putExtra(Constants.EXTRA_DATA, extras)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.base.BaseFragment
|
||||
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.SharedPreferenceManager
|
||||
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.coupon.CanCouponActivity
|
||||
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.profile.ProfileUpdateActivity
|
||||
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.notification.MemberRole
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@UnstableApi
|
||||
class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflate) {
|
||||
|
||||
companion object {
|
||||
private const val FUNCTION_BUTTON_SPAN_COUNT = 4
|
||||
}
|
||||
|
||||
private val viewModel: MyPageViewModel by inject()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private val functionButtonAdapter = FunctionButtonAdapter()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -187,6 +196,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
binding.llProfileLoginContainer.visibility = View.GONE
|
||||
|
||||
binding.llFunctionButtonGrid.visibility = View.VISIBLE
|
||||
setupFunctionButtonGrid()
|
||||
|
||||
binding.ivSettings.setOnClickListener {
|
||||
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) {
|
||||
binding.tvMyChannel.visibility = View.VISIBLE
|
||||
binding.tvMyChannel.setOnClickListener {
|
||||
@@ -335,6 +267,8 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
} else {
|
||||
binding.tvMyChannel.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateFunctionButtons()
|
||||
} else {
|
||||
binding.ivSettings.visibility = View.GONE
|
||||
binding.llFunctionButtonGrid.visibility = View.GONE
|
||||
@@ -342,19 +276,19 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
binding.rlProfileContainer.visibility = View.GONE
|
||||
binding.llProfileLoginContainer.visibility = View.VISIBLE
|
||||
binding.llProfileLoginContainer.setOnClickListener {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
showLoginActivity()
|
||||
}
|
||||
|
||||
binding.tvCanAmount.text =
|
||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||
binding.tvCanAmount.setOnClickListener {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
showLoginActivity()
|
||||
}
|
||||
|
||||
binding.tvPointAmount.text =
|
||||
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
|
||||
binding.tvPointAmount.setOnClickListener {
|
||||
(requireActivity() as MainActivity).showLoginActivity()
|
||||
showLoginActivity()
|
||||
}
|
||||
|
||||
binding.tvChargeCan.visibility = View.INVISIBLE
|
||||
@@ -380,58 +314,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
}
|
||||
|
||||
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) {
|
||||
crossfade(true)
|
||||
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.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() {
|
||||
Auth.auth(requireActivity(), requireContext()) { json ->
|
||||
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)
|
||||
intent?.data?.let { handlePayverseDeeplink(it) }
|
||||
intent.data?.let { handlePayverseDeeplink(it) }
|
||||
}
|
||||
|
||||
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.databinding.ActivityCanStatusBinding
|
||||
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.status.charge.CanChargeStatusFragment
|
||||
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
|
||||
|
||||
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
||||
@@ -137,7 +137,7 @@ class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
|
||||
}
|
||||
|
||||
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_SINGLE_TOP)
|
||||
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.databinding.ActivityPointStatusBinding
|
||||
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.use.PointUseStatusFragment
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
||||
@@ -120,7 +120,7 @@ class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
|
||||
}
|
||||
|
||||
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_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.base.BaseActivity
|
||||
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.following.FollowingCreatorAdapter
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationReceiveSettingsBinding>(
|
||||
ActivityNotificationReceiveSettingsBinding::inflate
|
||||
@@ -53,6 +57,33 @@ class NotificationReceiveSettingsActivity : BaseActivity<ActivityNotificationRec
|
||||
binding.ivMessage.setOnClickListener { viewModel.toggleMessage() }
|
||||
|
||||
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() {
|
||||
|
||||
@@ -20,7 +20,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
|
||||
@@ -174,7 +174,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
|
||||
private fun showMainActivity(extras: Bundle?) {
|
||||
handler.postDelayed({
|
||||
startActivity(
|
||||
Intent(applicationContext, MainActivity::class.java).apply {
|
||||
Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_DATA, extras)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.user.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -45,9 +44,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
|
||||
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
import androidx.core.net.toUri
|
||||
@@ -434,7 +433,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
|
||||
}
|
||||
|
||||
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)
|
||||
?: if (intent.extras != null) {
|
||||
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.LoadingDialog
|
||||
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.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -152,7 +152,7 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
|
||||
}
|
||||
|
||||
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)
|
||||
?: if (intent.extras != null) {
|
||||
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" />
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -80,6 +80,15 @@
|
||||
android:layout_height="1dp"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
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>
|
||||
</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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -49,6 +49,14 @@
|
||||
android:clipToPadding="false"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/ll_popular_characters"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -253,7 +253,8 @@
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/img_banner_audition" />
|
||||
android:src="@drawable/img_banner_audition"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_series_day_of_week"
|
||||
|
||||
@@ -143,11 +143,19 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp"
|
||||
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
|
||||
android:id="@+id/ll_replay_live"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -187,6 +187,14 @@
|
||||
app:drawableStartCompat="@drawable/ic_live_detail_bottom" />
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
||||
@@ -276,86 +276,13 @@
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- First Row -->
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_function_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<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>
|
||||
android:nestedScrollingEnabled="false"
|
||||
tools:itemCount="8"
|
||||
tools:listitem="@layout/item_function_button" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
@@ -419,6 +346,7 @@
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
android:id="@+id/rv_original"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/item_original_work" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -13,11 +13,20 @@
|
||||
android:nestedScrollingEnabled="false"
|
||||
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
|
||||
android:id="@+id/rv_series_by_genre"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -13,11 +13,20 @@
|
||||
android:nestedScrollingEnabled="false"
|
||||
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
|
||||
android:id="@+id/rv_series_day_of_week"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -79,6 +79,14 @@
|
||||
android:paddingHorizontal="24dp" />
|
||||
</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
|
||||
android:id="@+id/ll_recommend_series"
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
android:textSize="20sp"
|
||||
android:fontFamily="@font/regular"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</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"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
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:id="@+id/ll_rewarded_ad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/bg_buy_button"
|
||||
android:background="@drawable/bg_chat_quota_rewarded_ad_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_price"
|
||||
android:textColor="#263238"
|
||||
android:text="@string/chat_quota_rewarded_ad_label"
|
||||
android:textColor="@color/color_37474f"
|
||||
android:textSize="24sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:fontFamily="@font/bold"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:fontFamily="@font/medium"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/chat_quota_purchase_cta"
|
||||
android:textColor="#263238"
|
||||
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_rewarded_ad_chat_count"
|
||||
android:textColor="@color/color_37474f"
|
||||
android:textSize="18sp" />
|
||||
</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>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_weight="1"
|
||||
android:gravity="center"
|
||||
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>
|
||||