...
This commit is contained in:
parent
5670ece56d
commit
f794e24f24
@ -154,8 +154,17 @@ dependencies {
|
||||
implementation("io.github.junkfood02.youtubedl-android:library:0.17.4")
|
||||
implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.17.4")
|
||||
implementation("io.github.junkfood02.youtubedl-android:aria2c:0.17.4")
|
||||
|
||||
implementation("io.coil-kt:coil:2.5.0")
|
||||
implementation ("com.squareup.okhttp3:logging-interceptor:4.11.0")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
|
||||
|
||||
// Fragment KTX: Fragment에서 by viewModels() 델리게이트를 사용하기 위해 필수
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.1")
|
||||
|
||||
// Lifecycle KTX: viewLifecycleOwner.lifecycleScope 등을 사용하기 위해 필요
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||
|
||||
// implementation ("com.arthenica:ffmpeg-kit-full:4.5.LTS")
|
||||
// implementation ("com.arthenica:ffmpeg-kit-full:6.0-2")
|
||||
|
||||
|
||||
@ -55,6 +55,16 @@ port.onMessage.addListener(response => {
|
||||
var type= response["type"];
|
||||
|
||||
switch (type) {
|
||||
case "fetchAllImages": {
|
||||
const targetSrc = response["targetSrc"];
|
||||
const imageUrls = findAllRelatedImages(targetSrc);
|
||||
// 찾은 이미지 URL 목록을 'allImagesFound' 타입으로 네이티브 앱에 다시 전송
|
||||
sendMessage({
|
||||
type: "allImagesFound",
|
||||
urls: imageUrls
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "search" : {
|
||||
var keyword = response["keyword"];
|
||||
if (location.href.search("bigkinds.or.kr") > -1) {
|
||||
@ -154,6 +164,45 @@ port.onMessage.addListener(response => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* [신규 추가] 특정 이미지와 관련된 모든 이미지의 URL을 찾는 함수
|
||||
* @param {string} targetSrc - 사용자가 길게 누른 이미지의 소스 URL
|
||||
* @returns {string[]} - 찾아낸 이미지 URL의 배열
|
||||
*/
|
||||
function findAllRelatedImages(targetSrc) {
|
||||
// 1. 페이지 내의 모든 'img' 태그를 찾습니다.
|
||||
const allImages = Array.from(document.querySelectorAll('img'));
|
||||
|
||||
// 2. 사용자가 클릭한 이미지 엘리먼트를 찾습니다.
|
||||
const targetImage = allImages.find(img => img.src === targetSrc);
|
||||
|
||||
if (!targetImage) {
|
||||
// 클릭한 이미지를 찾지 못하면, 보이는 모든 이미지라도 반환
|
||||
return allImages.map(img => img.src).filter(src => src && src.startsWith('http'));
|
||||
}
|
||||
|
||||
// 3. 가장 가까운 공통 부모 컨테이너를 찾습니다. (예: article, div.post-body 등)
|
||||
// 이 로직은 웹사이트의 구조에 따라 고도화될 수 있습니다.
|
||||
let parent = targetImage.parentElement;
|
||||
for (let i = 0; i < 5; i++) { // 최대 5단계 위로 탐색
|
||||
if (parent.querySelectorAll('img').length > 1) {
|
||||
break; // 다른 이미지를 포함하는 부모를 찾으면 중단
|
||||
}
|
||||
if (parent.tagName.toLowerCase() === 'body') break;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
// 4. 해당 컨테이너 내의 모든 이미지 URL을 수집합니다.
|
||||
const relatedImages = Array.from(parent.querySelectorAll('img'));
|
||||
const validUrls = relatedImages
|
||||
.map(img => img.src)
|
||||
.filter(src => src && src.startsWith('http')); // 유효한 http(s) URL만 필터링
|
||||
|
||||
// 중복 제거 후 반환
|
||||
return [...new Set(validUrls)];
|
||||
}
|
||||
|
||||
|
||||
if (document.querySelector(".show_viewer") !== null) {
|
||||
document.querySelector(".show_viewer").click();
|
||||
sendMessage({type: "SHOWVIEWER"});
|
||||
|
||||
@ -2,10 +2,18 @@ package bums.lunatic.launcher
|
||||
|
||||
// ui/bookmark/BookmarkUploader.kt
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import bums.lunatic.launcher.home.adapters.VoteResponse
|
||||
import bums.lunatic.launcher.home.adapters.WebBookmark
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@ -233,4 +241,227 @@ object BookmarkUploader {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val saveBookmarkWithContentUrl = "$baseUrl/api/bookmarks/with-content"
|
||||
|
||||
// ... loginAndGetToken, saveBookmarkWithOkHttp 함수 ...
|
||||
|
||||
/**
|
||||
* [신규 추가] 여러 이미지와 함께 북마크를 저장하는 함수
|
||||
* @param pageUrl 원본 페이지의 URL
|
||||
* @param imageUrls 업로드할 이미지들의 URL 목록
|
||||
* @param comment 사용자가 작성한 코멘트
|
||||
* @param visibility 공개 범위 (PUBLIC, MEMBERS, PRIVATE)
|
||||
*/
|
||||
fun saveBookmarkWithContent(
|
||||
pageUrl: String,
|
||||
imageUrls: List<String>,
|
||||
comment: String,
|
||||
visibility: String
|
||||
) {
|
||||
if (userJwtToken == null) {
|
||||
println("🛑 저장을 위해 로그인이 필요합니다.")
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// --- 1단계: URL 목록의 모든 이미지를 병렬로 다운로드 ---
|
||||
val downloadedImages = imageUrls.map { imageUrl ->
|
||||
async { downloadImage(imageUrl, pageUrl) }
|
||||
}.awaitAll().filterNotNull() // 다운로드 성공한 것만 필터링
|
||||
|
||||
if (downloadedImages.isEmpty()) {
|
||||
println("❌ 모든 이미지 다운로드에 실패했습니다.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// --- 2단계: Multipart 요청 생성 ---
|
||||
val bookmarkDataMap = mapOf(
|
||||
"url" to pageUrl,
|
||||
"bookmarkType" to "IMAGE", // 북마크 타입을 IMAGE로 지정
|
||||
"userComment" to comment,
|
||||
"visibility" to visibility
|
||||
)
|
||||
val bookmarkDataJson = Gson().toJson(bookmarkDataMap)
|
||||
|
||||
val multipartBodyBuilder = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("bookmarkData", bookmarkDataJson)
|
||||
|
||||
// 다운로드된 각 이미지를 'files'라는 파트 이름으로 추가
|
||||
downloadedImages.forEachIndexed { index, imageData ->
|
||||
multipartBodyBuilder.addFormDataPart(
|
||||
"files", // 서버의 @RequestPart("files")와 일치해야 함
|
||||
"image_$index.jpg",
|
||||
imageData.toRequestBody("image/jpeg".toMediaTypeOrNull())
|
||||
)
|
||||
}
|
||||
|
||||
val multipartBody = multipartBodyBuilder.build()
|
||||
|
||||
// --- 3단계: 서버에 업로드 ---
|
||||
val formattedToken = "Bearer $userJwtToken"
|
||||
val request = Request.Builder()
|
||||
.url(saveBookmarkWithContentUrl)
|
||||
.addHeader("Authorization", formattedToken)
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
OkHttpClientInstance.client.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
println("✅ 여러 이미지와 함께 북마크 저장 성공: ${response.body?.string()}")
|
||||
} else {
|
||||
println("❌ 여러 이미지와 함께 북마크 저장 실패: ${response.code} ${response.message}")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("🔥 북마크 저장(여러 이미지 포함) 중 오류 발생: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(userId: String, userPw: String): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val loginDataMap = mapOf("userId" to userId, "userPw" to userPw)
|
||||
val json = Gson().toJson(loginDataMap)
|
||||
val requestBody = json.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
val request = Request.Builder().url(loginUrl).post(requestBody).build()
|
||||
|
||||
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("❌ 로그인 실패: ${response.code} ${response.message}")
|
||||
return@withContext false
|
||||
}
|
||||
response.body?.string()
|
||||
}
|
||||
|
||||
if (responseJson != null) {
|
||||
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||
val resultMap: Map<String, String> = Gson().fromJson(responseJson, type)
|
||||
userJwtToken = resultMap["token"]
|
||||
|
||||
if (userJwtToken != null) {
|
||||
println("✅ 로그인 성공! 토큰을 발급받았습니다.")
|
||||
return@withContext true
|
||||
}
|
||||
}
|
||||
println("❌ 로그인 실패: 응답에 토큰이 없습니다.")
|
||||
return@withContext false
|
||||
} catch (e: Exception) {
|
||||
println("🔥 로그인 중 오류 발생: ${e.message}")
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
fun getJwtToken(): String? = userJwtToken
|
||||
}
|
||||
|
||||
// Spring Page<> 응답과 일치하는 데이터 클래스
|
||||
data class Page<T>(
|
||||
val content: List<T>,
|
||||
val totalElements: Long,
|
||||
val number: Int,
|
||||
val size: Int
|
||||
)
|
||||
|
||||
class BookmarkApiService {
|
||||
|
||||
private val baseUrl = "https://lunaticbum.kr"
|
||||
private val bookmarksUrl = "$baseUrl/api/bookmarks/list?page=0&size=20" // 20개씩 가져오도록 설정
|
||||
|
||||
private val bookmarkLikeUrl = "$baseUrl/bookmarks/{bookmarkId}/like"
|
||||
private val bookmarkUnlikeUrl = "$baseUrl/bookmarks/{bookmarkId}/unlike"
|
||||
|
||||
|
||||
/**
|
||||
* 서버에서 북마크 목록을 가져오는 suspend 함수
|
||||
* @return 성공 시 WebBookmark 리스트, 실패 시 null 반환
|
||||
*/
|
||||
suspend fun fetchBookmarks(): List<WebBookmark>? = withContext(Dispatchers.IO) {
|
||||
val token = BookmarkUploader.getJwtToken() // 인증 토큰 가져오기
|
||||
if (token == null) {
|
||||
println("❌ 북마크 로딩 실패: 토큰이 없습니다.")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(bookmarksUrl)
|
||||
.addHeader("Authorization", "Bearer $token")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("❌ 북마크 API 호출 실패: ${response.code} ${response.message}")
|
||||
return@withContext null
|
||||
}
|
||||
response.body?.string()
|
||||
}
|
||||
|
||||
if (responseJson != null) {
|
||||
val pageType = object : TypeToken<Page<WebBookmark>>() {}.type
|
||||
val page: Page<WebBookmark> = Gson().fromJson(responseJson, pageType)
|
||||
println("✅ 북마크 ${page.content.size}개 로딩 성공!")
|
||||
return@withContext page.content // Page 객체에서 실제 북마크 목록만 반환
|
||||
}
|
||||
return@withContext null
|
||||
} catch (e: Exception) {
|
||||
println("🔥 북마크 로딩 중 오류 발생: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun likeBookmark(bookmarkId: String): VoteResponse? = withContext(Dispatchers.IO) {
|
||||
val token = BookmarkUploader.getJwtToken() ?: return@withContext null
|
||||
try {
|
||||
// POST 요청 시 빈 RequestBody가 필요합니다.
|
||||
val requestBody = "".toRequestBody("application/json".toMediaTypeOrNull())
|
||||
val request = Request.Builder()
|
||||
.url(bookmarkLikeUrl.replace("{bookmarkId}", bookmarkId))
|
||||
.addHeader("Authorization", "Bearer $token")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) return@withContext null
|
||||
response.body?.string()
|
||||
}
|
||||
return@withContext Gson().fromJson(responseJson, VoteResponse::class.java)
|
||||
} catch (e: Exception) {
|
||||
println("🔥 좋아요 요청 중 오류 발생: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 북마크에 '싫어요'를 요청하는 함수
|
||||
*/
|
||||
suspend fun unlikeBookmark(bookmarkId: String): VoteResponse? = withContext(Dispatchers.IO) {
|
||||
// ... likeBookmark와 거의 동일한 로직, URL만 다름 ...
|
||||
val token = BookmarkUploader.getJwtToken() ?: return@withContext null
|
||||
try {
|
||||
val requestBody = "".toRequestBody("application/json".toMediaTypeOrNull())
|
||||
val request = Request.Builder()
|
||||
.url(bookmarkUnlikeUrl.replace("{bookmarkId}", bookmarkId))
|
||||
.addHeader("Authorization", "Bearer $token")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) return@withContext null
|
||||
response.body?.string()
|
||||
}
|
||||
return@withContext Gson().fromJson(responseJson, VoteResponse::class.java)
|
||||
} catch (e: Exception) {
|
||||
println("🔥 싫어요 요청 중 오류 발생: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -57,6 +57,8 @@ import bums.lunatic.launcher.helpers.ForeGroundService
|
||||
import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver
|
||||
import bums.lunatic.launcher.home.GeckoWeb
|
||||
import bums.lunatic.launcher.home.RssHome
|
||||
import bums.lunatic.launcher.home.adapters.BookmarkDetailFragment
|
||||
import bums.lunatic.launcher.home.adapters.BookmarkPagerFragment
|
||||
import bums.lunatic.launcher.model.RssData
|
||||
import bums.lunatic.launcher.model.RssDataType
|
||||
import bums.lunatic.launcher.receiver.NLService
|
||||
@ -506,9 +508,10 @@ open class LauncherActivity : CommonActivity() {
|
||||
.replace(R.id.fragment_container, Novels())
|
||||
.commit()
|
||||
}
|
||||
|
||||
R.id.webtoons ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, Webtoons())
|
||||
.replace(R.id.fragment_container, BookmarkPagerFragment())
|
||||
.commit()
|
||||
}
|
||||
R.id.comics ->{
|
||||
|
||||
@ -490,18 +490,105 @@ class GeckoWeb : BWebview {
|
||||
super.onExternalResponse(session, response)
|
||||
}
|
||||
|
||||
private fun startBookmarkSaveProcess(pageUrl: String, mediaUrl: String) {
|
||||
// 1. 로그인 상태 확인
|
||||
if (BookmarkUploader.isUserLoggedIn) {
|
||||
// 이미 로그인 되어 있으면, 바로 북마크 정보 입력창 표시
|
||||
showBookmarkDetailsDialog(pageUrl, mediaUrl)
|
||||
// private fun startBookmarkSaveProcess(pageUrl: String, mediaUrl: String) {
|
||||
// // 1. 로그인 상태 확인
|
||||
// if (BookmarkUploader.isUserLoggedIn) {
|
||||
// // 이미 로그인 되어 있으면, 바로 북마크 정보 입력창 표시
|
||||
// showBookmarkDetailsDialog(pageUrl, mediaUrl)
|
||||
// } else {
|
||||
// // 로그인이 안 되어 있으면, 로그인 창을 먼저 표시
|
||||
// showLoginDialog {
|
||||
// // 로그인 성공 시 콜백으로 북마크 정보 입력창 표시
|
||||
// showBookmarkDetailsDialog(pageUrl, mediaUrl)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
override fun onContextMenu(
|
||||
session: GeckoSession,
|
||||
screenX: Int,
|
||||
screenY: Int,
|
||||
element: GeckoSession.ContentDelegate.ContextElement
|
||||
) {
|
||||
|
||||
|
||||
val pageUrl = element.baseUri ?: lastedUrl ?: return // 페이지 URL
|
||||
var mediaUrl = element.srcUri ?: return // 이미지 또는 비디오 URL
|
||||
|
||||
// 컨텍스트 메뉴에 '북마크 저장' 옵션 추가
|
||||
val menuItems = listOf("이 미디어만 북마크", "페이지의 모든 이미지 북마크", "다운로드")
|
||||
|
||||
val isMediaElement = element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE ||
|
||||
element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO
|
||||
if (!isMediaElement) {
|
||||
super.onContextMenu(session, screenX, screenY, element)
|
||||
return
|
||||
}
|
||||
// if (isMediaElement) {
|
||||
// menuItems.add("이 미디어 북마크하기")
|
||||
// menuItems.add("이 미디어 다운로드하기")
|
||||
// }
|
||||
//
|
||||
// if (menuItems.isEmpty()) {
|
||||
// super.onContextMenu(session, screenX, screenY, element)
|
||||
// return
|
||||
// }
|
||||
fun show(pageUrl : String,mediaUrl : String) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("작업 선택")
|
||||
.setItems(menuItems.toTypedArray()) { _, which ->
|
||||
when (menuItems[which]) {
|
||||
"이 미디어만 북마크" -> {
|
||||
// 기존 로직: 단일 이미지 북마크 프로세스 시작
|
||||
startBookmarkSaveProcessForSingleImage(pageUrl, mediaUrl)
|
||||
}
|
||||
"페이지의 모든 이미지 북마크" -> {
|
||||
// [신규 로직] WebExtension에 모든 이미지 URL 요청
|
||||
val message = JSONObject()
|
||||
message.put("type", "fetchAllImages")
|
||||
message.put("targetSrc", mediaUrl) // 기준이 되는 이미지 URL 전달
|
||||
mPort?.postMessage(message)
|
||||
}
|
||||
"다운로드" -> {
|
||||
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(pageUrl), mediaUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (element.baseUri?.contains("youtube") == true) {
|
||||
// lastedUrl?.let { videoUrl ->
|
||||
// lastedUrl?.let {
|
||||
// videoDlownLoad(it)
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
// 로그인이 안 되어 있으면, 로그인 창을 먼저 표시
|
||||
showLoginDialog {
|
||||
// 로그인 성공 시 콜백으로 북마크 정보 입력창 표시
|
||||
showBookmarkDetailsDialog(pageUrl, mediaUrl)
|
||||
Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}")
|
||||
// if (!BookmarkUploader.isUserLoggedIn) {
|
||||
BookmarkUploader.loginAndGetToken(
|
||||
userId = "lunaticbum",
|
||||
userPw = "VioPup*383",
|
||||
onSuccess = {
|
||||
show(pageUrl, mediaUrl)
|
||||
Toast.makeText(context, "로그인 성공!", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onError = { errorMessage ->
|
||||
Toast.makeText(context, "로그인 실패: $errorMessage", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
// } else {
|
||||
//// show(pageUrl, mediaUrl)
|
||||
// }
|
||||
}
|
||||
super.onContextMenu(session, screenX, screenY, element)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -545,7 +632,7 @@ class GeckoWeb : BWebview {
|
||||
/**
|
||||
* 북마크 저장을 위해 사용자로부터 코멘트와 공개 여부를 입력받는 다이얼로그 (수정된 버전)
|
||||
*/
|
||||
private fun showBookmarkDetailsDialog(pageUrl: String, mediaUrl: String) {
|
||||
private fun showBookmarkDetailsDialog(pageUrl: String, mediaUrls: List<String>) {
|
||||
val context = this@GeckoWeb.context ?: return
|
||||
val visibilityOptions = arrayOf("PUBLIC", "MEMBERS", "PRIVATE")
|
||||
|
||||
@ -599,8 +686,8 @@ class GeckoWeb : BWebview {
|
||||
val visibility = (radioGroup.findViewById<RadioButton>(checkedRadioButtonId))?.text.toString()
|
||||
|
||||
if (visibility.isNotEmpty()) {
|
||||
BookmarkUploader.saveBookmarkWithImageUpload(pageUrl, mediaUrl, comment, visibility)
|
||||
Toast.makeText(context, "[$visibility] 북마크로 저장했습니다.", Toast.LENGTH_SHORT).show()
|
||||
BookmarkUploader.saveBookmarkWithContent(pageUrl, mediaUrls, comment, visibility)
|
||||
Toast.makeText(context, "[$visibility] 북마크로 ${mediaUrls.size}개 이미지를 저장했습니다.", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "공개 범위가 선택되지 않았습니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -609,79 +696,29 @@ class GeckoWeb : BWebview {
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onContextMenu(
|
||||
session: GeckoSession,
|
||||
screenX: Int,
|
||||
screenY: Int,
|
||||
element: GeckoSession.ContentDelegate.ContextElement
|
||||
) {
|
||||
|
||||
|
||||
val pageUrl = element.baseUri ?: lastedUrl ?: return // 페이지 URL
|
||||
var mediaUrl = element.srcUri ?: return // 이미지 또는 비디오 URL
|
||||
|
||||
// 컨텍스트 메뉴에 '북마크 저장' 옵션 추가
|
||||
val menuItems = mutableListOf<String>()
|
||||
val isMediaElement = element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE ||
|
||||
element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO
|
||||
|
||||
if (isMediaElement) {
|
||||
menuItems.add("이 미디어 북마크하기")
|
||||
menuItems.add("이 미디어 다운로드하기")
|
||||
}
|
||||
|
||||
if (menuItems.isEmpty()) {
|
||||
super.onContextMenu(session, screenX, screenY, element)
|
||||
return
|
||||
}
|
||||
fun show(pageUrl : String,mediaUrl : String) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("작업 선택")
|
||||
.setItems(menuItems.toTypedArray()) { _, which ->
|
||||
when (menuItems[which]) {
|
||||
"이 미디어 북마크하기" -> {
|
||||
// 북마크 저장 프로세스 시작
|
||||
startBookmarkSaveProcess(pageUrl, mediaUrl)
|
||||
}
|
||||
"이 미디어 다운로드하기" -> {
|
||||
// 기존의 파일 다운로드 로직 실행
|
||||
val finalMediaUrl = if (mediaUrl.contains("dcimg")) replaceDcUrl(mediaUrl) else mediaUrl
|
||||
CommonUtils.downloadFileWithOkHttp(context, Uri.parse(pageUrl), finalMediaUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (element.baseUri?.contains("youtube") == true) {
|
||||
// lastedUrl?.let { videoUrl ->
|
||||
// lastedUrl?.let {
|
||||
// videoDlownLoad(it)
|
||||
// }
|
||||
// }
|
||||
private fun startBookmarkSaveProcessForSingleImage(pageUrl: String, mediaUrl: String) {
|
||||
if (BookmarkUploader.isUserLoggedIn) {
|
||||
showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl)) // URL을 리스트로 감싸서 전달
|
||||
} else {
|
||||
Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}")
|
||||
// if (!BookmarkUploader.isUserLoggedIn) {
|
||||
BookmarkUploader.loginAndGetToken(
|
||||
userId = "lunaticbum",
|
||||
userPw = "VioPup*383",
|
||||
onSuccess = {
|
||||
show(pageUrl, mediaUrl)
|
||||
Toast.makeText(context, "로그인 성공!", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onError = { errorMessage ->
|
||||
Toast.makeText(context, "로그인 실패: $errorMessage", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
// } else {
|
||||
//// show(pageUrl, mediaUrl)
|
||||
// }
|
||||
}
|
||||
super.onContextMenu(session, screenX, screenY, element)
|
||||
showLoginDialog {
|
||||
showBookmarkDetailsDialog(pageUrl, listOf(mediaUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [신규 추가] 여러 이미지 북마크 저장 프로세스를 시작하는 함수
|
||||
private fun startBookmarkSaveProcessForMultipleImages(pageUrl: String, mediaUrls: List<String>) {
|
||||
if (BookmarkUploader.isUserLoggedIn) {
|
||||
showBookmarkDetailsDialog(pageUrl, mediaUrls)
|
||||
} else {
|
||||
showLoginDialog {
|
||||
showBookmarkDetailsDialog(pageUrl, mediaUrls)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [신규 추가] 단일 이미지 북마크 저장 프로세스를 시작하는 함수
|
||||
|
||||
|
||||
val progressDelegate = object : GeckoSession.ProgressDelegate {
|
||||
override fun onSecurityChange(
|
||||
session: GeckoSession,
|
||||
@ -888,6 +925,11 @@ class GeckoWeb : BWebview {
|
||||
var lPortMessage =
|
||||
Gson().fromJson<PortMessage>(message, PortMessage::class.java)
|
||||
when(lPortMessage.type) {
|
||||
"allImagesFound" -> {
|
||||
lastedUrl?.let {
|
||||
startBookmarkSaveProcessForMultipleImages(it, lPortMessage.urls)
|
||||
}
|
||||
}
|
||||
"SINGLE_IMAGE_DATA"-> {
|
||||
if (lPortMessage.imgSrc != null && lPortMessage.base64Data != null) {
|
||||
// *** 핵심: 수신한 Base64 데이터를 커스텀 캐시에 저장 ***
|
||||
|
||||
@ -0,0 +1,400 @@
|
||||
package bums.lunatic.launcher.home.adapters
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import bums.lunatic.launcher.BookmarkApiService
|
||||
import bums.lunatic.launcher.BookmarkUploader
|
||||
import bums.lunatic.launcher.R
|
||||
import coil.load
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
// bums.lunatic.launcher/ui/BookmarkPagerFragment.kt
|
||||
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import bums.lunatic.launcher.utils.Blog
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BookmarkPagerFragment : Fragment() {
|
||||
|
||||
// by viewModels() 델리게이트를 사용해 ViewModel 인스턴스 생성
|
||||
private val viewModel: BookmarkViewModel by viewModels()
|
||||
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var loginLayout: LinearLayout
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var errorTextView: TextView
|
||||
private lateinit var userIdInput: EditText
|
||||
private lateinit var userPwInput: EditText
|
||||
private lateinit var loginButton: Button
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_bookmark_pager, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 뷰 초기화
|
||||
viewPager = view.findViewById(R.id.viewPager)
|
||||
loginLayout = view.findViewById(R.id.loginLayout)
|
||||
progressBar = view.findViewById(R.id.progressBar)
|
||||
errorTextView = view.findViewById(R.id.errorTextView)
|
||||
userIdInput = view.findViewById(R.id.userIdInput)
|
||||
userPwInput = view.findViewById(R.id.userPwInput)
|
||||
loginButton = view.findViewById(R.id.loginButton)
|
||||
|
||||
// 로그인 버튼 클릭 리스너 설정
|
||||
loginButton.setOnClickListener {
|
||||
val userId = userIdInput.text.toString().trim()
|
||||
val userPw = userPwInput.text.toString().trim()
|
||||
if (userId.isNotEmpty() && userPw.isNotEmpty()) {
|
||||
viewModel.login(userId, userPw)
|
||||
} else {
|
||||
Toast.makeText(context, "아이디와 비밀번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// ViewModel의 UI 상태를 관찰하고, 상태에 따라 UI 업데이트
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.uiState.collect { state ->
|
||||
updateUi(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUi(state: UiState) {
|
||||
// 모든 뷰를 일단 숨김
|
||||
loginLayout.isVisible = false
|
||||
viewPager.isVisible = false
|
||||
progressBar.isVisible = false
|
||||
errorTextView.isVisible = false
|
||||
|
||||
// 상태에 따라 필요한 뷰만 표시
|
||||
when (state) {
|
||||
is UiState.LoggedOut -> {
|
||||
loginLayout.isVisible = true
|
||||
}
|
||||
is UiState.Loading -> {
|
||||
progressBar.isVisible = true
|
||||
}
|
||||
is UiState.Success -> {
|
||||
viewPager.isVisible = true
|
||||
setupViewPager(state.bookmarks)
|
||||
}
|
||||
is UiState.Error -> {
|
||||
errorTextView.isVisible = true
|
||||
errorTextView.text = state.message
|
||||
// 에러 발생 시 잠시 후 다시 로그인 폼을 보여줄 수 있음
|
||||
if(state.message.contains("아이디")) {
|
||||
loginLayout.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupViewPager(bookmarks: List<WebBookmark>) {
|
||||
viewPager.adapter = BookmarkPagerAdapter(this, bookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Visibility {
|
||||
PUBLIC, // 전체 공개
|
||||
MEMBERS, // 회원 공개
|
||||
PRIVATE // 비공개 (나만 보기)
|
||||
}
|
||||
|
||||
enum class MetadataStatus {
|
||||
PENDING, // 처리 대기 중
|
||||
COMPLETED, // 처리 완료
|
||||
FAILED // 처리 실패
|
||||
}
|
||||
|
||||
enum class BookmarkType {
|
||||
URL, // 기존 웹 페이지 링크
|
||||
IMAGE, // 하나 이상의 이미지
|
||||
VIDEO // 하나 이상의 비디오
|
||||
}
|
||||
data class WebBookmark(
|
||||
var id: String? = null,
|
||||
var userId: String, // 누가 저장했는지
|
||||
var url: String?, // 원본 페이지 URL
|
||||
// [신규] 북마크 타입 (URL, IMAGE, VIDEO 등)
|
||||
var bookmarkType: String = BookmarkType.URL.name,
|
||||
// [신규] 콘텐츠 URL 목록 (웹페이지는 1개, 이미지는 여러 개 가능)
|
||||
var contentUrls: List<String> = emptyList(),
|
||||
var title: String? = null, // 페이지 제목
|
||||
var description: String? = null, // 페이지 요약 (메타 태그)
|
||||
var thumbnailUrl: String? = null, // 페이지 썸네일 (메타 태그)
|
||||
var userComment: String? = null, // 사용자가 남긴 짧은 의견
|
||||
var tags: List<String>? = null, // 태그 (예: #kotlin, #spring)
|
||||
var savedAt: Long = System.currentTimeMillis(), // 저장 시간
|
||||
// [신규 추가] 공개 범위 필드. 기본값은 PRIVATE.
|
||||
var visibility: String = Visibility.PRIVATE.name,
|
||||
// [신규 추가] 좋아요/싫어요 카운트 필드
|
||||
var voteCount: Long = 0,
|
||||
var unlikeCount: Long = 0,
|
||||
var userSelectedImageUrl: String? = null,
|
||||
var metadataStatus: String = MetadataStatus.PENDING.name,
|
||||
|
||||
// [추가] 카테고리 필드 (하나만 가질 수 있도록 String으로 설정)
|
||||
var category: String? = null,
|
||||
val displayImageUrl: String
|
||||
)
|
||||
class BookmarkPagerAdapter(fragment: Fragment, private val bookmarks: List<WebBookmark>) : FragmentStateAdapter(fragment) {
|
||||
|
||||
override fun getItemCount(): Int = bookmarks.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
// 각 포지션에 맞는 북마크 데이터를 담아
|
||||
// BookmarkDetailFragment 인스턴스를 생성하여 반환합니다.
|
||||
return BookmarkDetailFragment.newInstance(bookmarks[position])
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkImageAdapter(private val imageUrls: List<String>) : RecyclerView.Adapter<BookmarkImageAdapter.ImageViewHolder>() {
|
||||
init {
|
||||
Blog.LOGE("imageUrls >>> ${imageUrls}")
|
||||
}
|
||||
class ImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView = view.findViewById(R.id.bookmarkImageView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_bookmark_image, parent, false)
|
||||
return ImageViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
val imageUrl = imageUrls[position]
|
||||
// Coil을 사용해 URL로부터 이미지를 로드하고 ImageView에 표시합니다.
|
||||
Blog.LOGE("imageUrl >>> ${imageUrl}")
|
||||
holder.imageView.load("https://lunaticbum.kr$imageUrl") {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_launcher) // 로딩 중 보여줄 이미지 (ic_placeholder.xml 같은 drawable 필요)
|
||||
error(R.drawable.ic_news) // 에러 시 보여줄 이미지 (ic_error.xml 같은 drawable 필요)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = imageUrls.size
|
||||
}
|
||||
|
||||
class BookmarkDetailFragment : Fragment() {
|
||||
|
||||
private lateinit var bookmark: WebBookmark
|
||||
private val viewModel: BookmarkViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// newInstance를 통해 전달된 북마크 데이터를 arguments에서 꺼냅니다.
|
||||
arguments?.getString(ARG_BOOKMARK)?.let {
|
||||
bookmark = Gson().fromJson(it, WebBookmark::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// XML 레이아웃을 inflate 합니다.
|
||||
return inflater.inflate(R.layout.fragment_bookmark_detail, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// View들을 초기화하고 북마크 데이터를 바인딩합니다.
|
||||
val titleTextView = view.findViewById<TextView>(R.id.bookmarkTitle)
|
||||
val commentTextView = view.findViewById<TextView>(R.id.bookmarkComment)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.contentRecyclerView)
|
||||
|
||||
titleTextView.text = bookmark.title ?: "제목 없음"
|
||||
commentTextView.text = bookmark.userComment ?: ""
|
||||
commentTextView.visibility = if (bookmark.userComment.isNullOrBlank()) View.GONE else View.VISIBLE
|
||||
|
||||
val likeButton = view.findViewById<Button>(R.id.likeButton)
|
||||
val unlikeButton = view.findViewById<Button>(R.id.unlikeButton)
|
||||
val commentButton = view.findViewById<Button>(R.id.commentButton)
|
||||
val likeCountText = view.findViewById<TextView>(R.id.likeCountText)
|
||||
val unlikeCountText = view.findViewById<TextView>(R.id.unlikeCountText)
|
||||
|
||||
// 초기 카운트 설정
|
||||
likeCountText.text = bookmark.voteCount.toString()
|
||||
unlikeCountText.text = bookmark.unlikeCount.toString()
|
||||
|
||||
// 버튼 클릭 리스너 설정
|
||||
likeButton.setOnClickListener {
|
||||
bookmark.id?.let { viewModel.likeBookmark(it) }
|
||||
}
|
||||
|
||||
unlikeButton.setOnClickListener {
|
||||
bookmark.id?.let { viewModel.unlikeBookmark(it) }
|
||||
}
|
||||
|
||||
commentButton.setOnClickListener {
|
||||
// TODO: 댓글 화면(BottomSheetDialog 또는 새 Fragment)을 띄우는 로직 구현
|
||||
Toast.makeText(context, "댓글 기능 구현 예정", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// UI 상태가 변경될 때마다(예: 다른 사용자가 좋아요를 눌렀을 때) 카운트 업데이트
|
||||
// 이 코드는 ViewPager 스와이프 시에도 현재 북마크의 최신 카운트를 반영해줍니다.
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.uiState.collect { state ->
|
||||
if (state is UiState.Success) {
|
||||
// 현재 프래그먼트의 북마크 ID와 일치하는 최신 데이터를 찾음
|
||||
val latestBookmark = state.bookmarks.find { it.id == bookmark.id }
|
||||
latestBookmark?.let {
|
||||
likeCountText.text = it.voteCount.toString()
|
||||
unlikeCountText.text = it.unlikeCount.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecyclerView 설정
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
// contentUrls에 있는 이미지 목록으로 어댑터를 생성하고 RecyclerView에 연결합니다.
|
||||
recyclerView.adapter = BookmarkImageAdapter(bookmark.contentUrls)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_BOOKMARK = "bookmark_json"
|
||||
|
||||
// Fragment 생성 시 데이터를 안전하게 전달하기 위한 newInstance 패턴
|
||||
@JvmStatic
|
||||
fun newInstance(bookmark: WebBookmark) =
|
||||
BookmarkDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// 북마크 객체를 JSON 문자열로 변환하여 Bundle에 담습니다.
|
||||
putString(ARG_BOOKMARK, Gson().toJson(bookmark))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI 상태를 나타내는 sealed class
|
||||
sealed class UiState {
|
||||
object LoggedOut : UiState() // 로그아웃 상태 (로그인 폼 보여줌)
|
||||
object Loading : UiState() // 로딩 중 (프로그레스바 보여줌)
|
||||
data class Success(val bookmarks: List<WebBookmark>) : UiState() // 성공 (ViewPager 보여줌)
|
||||
data class Error(val message: String) : UiState() // 에러 (에러 메시지 보여줌)
|
||||
}
|
||||
|
||||
class BookmarkViewModel : ViewModel() {
|
||||
|
||||
private val apiService = BookmarkApiService()
|
||||
|
||||
// UI 상태를 관찰할 수 있는 StateFlow
|
||||
private val _uiState = MutableStateFlow<UiState>(UiState.LoggedOut)
|
||||
val uiState: StateFlow<UiState> = _uiState
|
||||
|
||||
init {
|
||||
// ViewModel 생성 시 로그인 상태 즉시 확인
|
||||
checkLoginStatus()
|
||||
}
|
||||
|
||||
private fun checkLoginStatus() {
|
||||
if (BookmarkUploader.isUserLoggedIn) {
|
||||
fetchBookmarks() // 이미 로그인 되어있으면 바로 데이터 로딩
|
||||
} else {
|
||||
_uiState.value = UiState.LoggedOut // 로그아웃 상태 유지
|
||||
}
|
||||
}
|
||||
|
||||
fun login(userId: String, userPw: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = UiState.Loading // 로딩 상태로 변경
|
||||
val success = BookmarkUploader.login(userId, userPw)
|
||||
if (success) {
|
||||
fetchBookmarks() // 로그인 성공 시 북마크 로딩
|
||||
} else {
|
||||
_uiState.value = UiState.Error("아이디 또는 비밀번호를 확인해주세요.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchBookmarks() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = UiState.Loading // 로딩 상태로 변경
|
||||
val bookmarks = apiService.fetchBookmarks()
|
||||
if (bookmarks != null) {
|
||||
if (bookmarks.isEmpty()){
|
||||
_uiState.value = UiState.Error("저장된 북마크가 없습니다.")
|
||||
} else {
|
||||
_uiState.value = UiState.Success(bookmarks) // 성공 상태와 데이터 전달
|
||||
}
|
||||
} else {
|
||||
_uiState.value = UiState.Error("북마크를 불러오는데 실패했습니다.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun likeBookmark(bookmarkId: String) {
|
||||
viewModelScope.launch {
|
||||
val response = apiService.likeBookmark(bookmarkId)
|
||||
response?.let {
|
||||
updateBookmarkCounts(bookmarkId, it.voteCount, it.unlikeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unlikeBookmark(bookmarkId: String) {
|
||||
viewModelScope.launch {
|
||||
val response = apiService.unlikeBookmark(bookmarkId)
|
||||
response?.let {
|
||||
updateBookmarkCounts(bookmarkId, it.voteCount, it.unlikeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 북마크 리스트에서 특정 북마크의 카운트를 업데이트하고 UI를 갱신하는 함수
|
||||
*/
|
||||
private fun updateBookmarkCounts(bookmarkId: String, newVoteCount: Long, newUnlikeCount: Long) {
|
||||
val currentState = _uiState.value
|
||||
if (currentState is UiState.Success) {
|
||||
// 현재 리스트에서 해당 북마크를 찾습니다.
|
||||
val updatedList = currentState.bookmarks.map { bookmark ->
|
||||
if (bookmark.id == bookmarkId) {
|
||||
// 카운트가 변경된 새 객체를 생성하여 교체합니다.
|
||||
bookmark.copy(voteCount = newVoteCount, unlikeCount = newUnlikeCount)
|
||||
} else {
|
||||
bookmark
|
||||
}
|
||||
}
|
||||
// 업데이트된 리스트로 UI 상태를 갱신합니다.
|
||||
_uiState.value = UiState.Success(updatedList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
data class VoteResponse(
|
||||
val voteCount: Long,
|
||||
val unlikeCount: Long
|
||||
)
|
||||
@ -15,7 +15,7 @@ class PortMessage {
|
||||
var privates : ArrayList<RssData>? = null
|
||||
var currentPage : String? = null
|
||||
var cookies : String? = null
|
||||
|
||||
var urls : List<String> = emptyList()
|
||||
var imgSrc: String? = null
|
||||
var base64Data: String? = null
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable/ic_comment.png
Normal file
BIN
app/src/main/res/drawable/ic_comment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 B |
BIN
app/src/main/res/drawable/ic_thumb_down.png
Normal file
BIN
app/src/main/res/drawable/ic_thumb_down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 B |
BIN
app/src/main/res/drawable/ic_thumb_up.png
Normal file
BIN
app/src/main/res/drawable/ic_thumb_up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 B |
73
app/src/main/res/layout/fragment_bookmark_detail.xml
Normal file
73
app/src/main/res/layout/fragment_bookmark_detail.xml
Normal file
@ -0,0 +1,73 @@
|
||||
<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="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmarkTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5"
|
||||
tools:text="북마크 제목" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmarkComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBody1"
|
||||
tools:text="사용자가 남긴 코멘트입니다." />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contentRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="16dp" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?attr/colorSurfaceContainer"> <com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/likeButton"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_thumb_up" /> <TextView
|
||||
android:id="@+id/likeCountText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
tools:text="12" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/unlikeButton"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
app:icon="@drawable/ic_thumb_down" /> <TextView
|
||||
android:id="@+id/unlikeCountText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
tools:text="3" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/commentButton"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_comment" /> </LinearLayout>
|
||||
</LinearLayout>
|
||||
63
app/src/main/res/layout/fragment_bookmark_pager.xml
Normal file
63
app/src/main/res/layout/fragment_bookmark_pager.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loginLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center"
|
||||
android:padding="24dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/userIdInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="아이디"
|
||||
android:inputType="text"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/userPwInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="비밀번호"
|
||||
android:inputType="textPassword"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/loginButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="로그인"/>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:text="에러 메시지"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
8
app/src/main/res/layout/item_bookmark_image.xml
Normal file
8
app/src/main/res/layout/item_bookmark_image.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<ImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/bookmarkImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:scaleType="fitCenter" />
|
||||
Loading…
x
Reference in New Issue
Block a user