..
This commit is contained in:
parent
768dd0ebf0
commit
5052d5cf52
@ -71,6 +71,7 @@ android {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
sourceSets["main"].jniLibs.srcDirs("src/main/jniLibs")
|
sourceSets["main"].jniLibs.srcDirs("src/main/jniLibs")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
@ -95,6 +96,7 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions.resources.excludes.add("META-INF/*")
|
packagingOptions.resources.excludes.add("META-INF/*")
|
||||||
packagingOptions.resources.excludes.add("mozilla/*")
|
packagingOptions.resources.excludes.add("mozilla/*")
|
||||||
packagingOptions.resources.excludes.add("META-INF/*/*")
|
packagingOptions.resources.excludes.add("META-INF/*/*")
|
||||||
@ -114,9 +116,16 @@ android {
|
|||||||
doNotStrip("**/libffmpeg.zip.so")
|
doNotStrip("**/libffmpeg.zip.so")
|
||||||
doNotStrip("**/libpython.zip.so")
|
doNotStrip("**/libpython.zip.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(fileTree(mapOf(
|
||||||
|
"dir" to "libs",
|
||||||
|
"include" to listOf("*.aar", "*.jar"),
|
||||||
|
)))
|
||||||
val kotlinVersion: String? by extra
|
val kotlinVersion: String? by extra
|
||||||
val realmVersion = "2.0.0"
|
val realmVersion = "2.0.0"
|
||||||
implementation ("androidx.appcompat:appcompat:1.7.1")
|
implementation ("androidx.appcompat:appcompat:1.7.1")
|
||||||
@ -172,12 +181,15 @@ dependencies {
|
|||||||
// Lifecycle KTX: viewLifecycleOwner.lifecycleScope 등을 사용하기 위해 필요
|
// Lifecycle KTX: viewLifecycleOwner.lifecycleScope 등을 사용하기 위해 필요
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// implementation ("com.arthenica:ffmpeg-kit-full:4.5.LTS")
|
// implementation ("com.arthenica:ffmpeg-kit-full:4.5.LTS")
|
||||||
// implementation ("com.arthenica:ffmpeg-kit-full:6.0-2")
|
// implementation ("com.arthenica:ffmpeg-kit-full:6.0-2")
|
||||||
|
|
||||||
// implementation(files("libs/ffmpeg-kit-full-6.0-2.LTS.aar"))
|
// implementation(files("libs/ffmpeg-kit-full-6.0-2.LTS.aar"))
|
||||||
// implementation("com.arthenica:ffmpeg-kit-full-gpl:5.1")
|
// implementation("com.arthenica:ffmpeg-kit-full-gpl:5.1")
|
||||||
|
|
||||||
|
|
||||||
implementation ("androidx.media:media:1.7.0")
|
implementation ("androidx.media:media:1.7.0")
|
||||||
// implementation(project(":sdk"))
|
// implementation(project(":sdk"))
|
||||||
// implementation ("me.everything:providers-android:1.0.1")
|
// implementation ("me.everything:providers-android:1.0.1")
|
||||||
|
|||||||
BIN
app/libs/jlibtorrent-2.0.12.7.jar
Normal file
BIN
app/libs/jlibtorrent-2.0.12.7.jar
Normal file
Binary file not shown.
BIN
app/libs/jlibtorrent-android-arm-2.0.12.7.jar
Normal file
BIN
app/libs/jlibtorrent-android-arm-2.0.12.7.jar
Normal file
Binary file not shown.
BIN
app/libs/jlibtorrent-android-arm64-2.0.12.7.jar
Normal file
BIN
app/libs/jlibtorrent-android-arm64-2.0.12.7.jar
Normal file
Binary file not shown.
BIN
app/libs/jlibtorrent-android-x86-2.0.12.7.jar
Normal file
BIN
app/libs/jlibtorrent-android-x86-2.0.12.7.jar
Normal file
Binary file not shown.
BIN
app/libs/jlibtorrent-android-x86_64-2.0.12.7.jar
Normal file
BIN
app/libs/jlibtorrent-android-x86_64-2.0.12.7.jar
Normal file
Binary file not shown.
@ -148,6 +148,12 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".workers.TorrentService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".wall.MyWallpaperService"
|
android:name=".wall.MyWallpaperService"
|
||||||
android:label="Bums Live Wallpaper"
|
android:label="Bums Live Wallpaper"
|
||||||
|
|||||||
@ -361,10 +361,26 @@ function toast(msg) {
|
|||||||
|
|
||||||
var mainContentsEl = null
|
var mainContentsEl = null
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
if (port) {
|
|
||||||
sendMessage({type: "MSG", msg: "connect prot"});
|
|
||||||
time1 = setTimeout(autoScrollAndSave(false), 3500)
|
|
||||||
|
|
||||||
|
const currentUrl = location.href;
|
||||||
|
|
||||||
|
// 여기에 제외하고 싶은 문자열 배열을 넣으면 됨 (예: 관리자 페이지, 로그인 페이지 등)
|
||||||
|
const excludedStrings = [
|
||||||
|
'booktoki',
|
||||||
|
'manatoki',
|
||||||
|
'x.com',
|
||||||
|
'newtoki',
|
||||||
|
'youtube',
|
||||||
|
'perplexity',
|
||||||
|
// 필요하면 더 추가
|
||||||
|
];
|
||||||
|
|
||||||
|
// currentUrl에 excludedStrings에 있는 문자열이 하나라도 포함되면 true
|
||||||
|
const shouldExclude = excludedStrings.some(str => currentUrl.includes(str));
|
||||||
|
|
||||||
|
if (port && !shouldExclude) {
|
||||||
|
toast("connect port on " + location.href);
|
||||||
|
time1 = setTimeout(autoScrollAndSave(false), 3500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -726,12 +742,12 @@ async function handleCommon() {
|
|||||||
.filter(src => src && src.startsWith('http'));
|
.filter(src => src && src.startsWith('http'));
|
||||||
const uniqueUrls = [...new Set(validImageUrls)];
|
const uniqueUrls = [...new Set(validImageUrls)];
|
||||||
|
|
||||||
console.log(`Found ${'$'}{uniqueUrls.length} unique images to cache.`);
|
// console.log(`Found ${'$'}{uniqueUrls.length} unique images to cache.`);
|
||||||
|
|
||||||
|
|
||||||
for (const image of uniqueUrls) {
|
// for (const image of uniqueUrls) {
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
// 3. 각 URL을 순회하며 Base64로 변환하고 즉시 네이티브로 전송
|
// 3. 각 URL을 순회하며 Base64로 변환하고 즉시 네이티브로 전송
|
||||||
// (모든 작업을 병렬로 처리하지 않고 순차적(또는 하나씩)으로 보내 메모리 부담을 줄임)
|
// (모든 작업을 병렬로 처리하지 않고 순차적(또는 하나씩)으로 보내 메모리 부담을 줄임)
|
||||||
var idx = 0
|
var idx = 0
|
||||||
@ -770,7 +786,7 @@ async function handleCommon() {
|
|||||||
// 현재 스크롤 위치 + 화면에 보이는 높이가 전체 페이지 높이보다 크거나 같으면
|
// 현재 스크롤 위치 + 화면에 보이는 높이가 전체 페이지 높이보다 크거나 같으면
|
||||||
// 페이지의 끝에 도달한 것입니다.
|
// 페이지의 끝에 도달한 것입니다.
|
||||||
if (Math.ceil(window.scrollY) + Math.ceil(window.innerHeight) >= (totalHeight * 0.9)) {
|
if (Math.ceil(window.scrollY) + Math.ceil(window.innerHeight) >= (totalHeight * 0.9)) {
|
||||||
console.log("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다.");
|
// console.log("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다.");
|
||||||
toast("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다.")
|
toast("✅ 페이지 끝에 도달했습니다. 스크롤을 중지합니다.")
|
||||||
clearInterval(scrollInterval); // 반복 실행을 멈춥니다.
|
clearInterval(scrollInterval); // 반복 실행을 멈춥니다.
|
||||||
getImg()
|
getImg()
|
||||||
@ -781,7 +797,7 @@ async function handleCommon() {
|
|||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'smooth' // 부드럽게 스크롤하는 옵션
|
behavior: 'smooth' // 부드럽게 스크롤하는 옵션
|
||||||
});
|
});
|
||||||
toast(`smooth scrollBy ${scrollIncrement}, ${window.scrollY}, ${window.innerHeight}, ${totalHeight}`)
|
// toast(`smooth scrollBy ${scrollIncrement}, ${window.scrollY}, ${window.innerHeight}, ${totalHeight}`)
|
||||||
}
|
}
|
||||||
}, 2000); // 1000밀리초 = 1초
|
}, 2000); // 1000밀리초 = 1초
|
||||||
}
|
}
|
||||||
|
|||||||
@ -425,7 +425,8 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
|
|||||||
for (item in scoredItems) {
|
for (item in scoredItems) {
|
||||||
if (item.type == "APP") {
|
if (item.type == "APP") {
|
||||||
val app = realm.query<AppInfo>("pkgName == $0", item.key).first().find()
|
val app = realm.query<AppInfo>("pkgName == $0", item.key).first().find()
|
||||||
if (app != null && !app.blockRecommend) {
|
|
||||||
|
if (app != null && (app.blockRecommend == false || (binding.hidden.isSelected == app.blockRecommend))) {
|
||||||
try { if(pm.getLaunchIntentForPackage(app.pkgName ?: "") != null) {
|
try { if(pm.getLaunchIntentForPackage(app.pkgName ?: "") != null) {
|
||||||
unifiedList.add(RecommendationItem.AppItem(realm.copyFromRealm(app)))
|
unifiedList.add(RecommendationItem.AppItem(realm.copyFromRealm(app)))
|
||||||
}} catch (e: Exception) { }
|
}} catch (e: Exception) { }
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -28,6 +30,7 @@ import android.widget.RadioGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContentProviderCompat.requireContext
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import bums.lunatic.launcher.BookmarkUploader
|
import bums.lunatic.launcher.BookmarkUploader
|
||||||
@ -43,6 +46,7 @@ import bums.lunatic.launcher.model.getRssData
|
|||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import bums.lunatic.launcher.utils.CommonUtils
|
import bums.lunatic.launcher.utils.CommonUtils
|
||||||
import bums.lunatic.launcher.utils.SimpleFingerGestures
|
import bums.lunatic.launcher.utils.SimpleFingerGestures
|
||||||
|
import bums.lunatic.launcher.workers.TorrentService
|
||||||
import bums.lunatic.launcher.workers.WorkersDb
|
import bums.lunatic.launcher.workers.WorkersDb
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@ -75,6 +79,7 @@ import java.io.FileOutputStream
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlin.jvm.java
|
||||||
|
|
||||||
// BWebview와 GeckoWeb을 통합한 클래스
|
// BWebview와 GeckoWeb을 통합한 클래스
|
||||||
open class GeckoWeb @JvmOverloads constructor(
|
open class GeckoWeb @JvmOverloads constructor(
|
||||||
@ -350,9 +355,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
markdownContents = null
|
markdownContents = null
|
||||||
markdownUri = null
|
markdownUri = null
|
||||||
if (url.startsWith("magnet:?")) {
|
if (url.startsWith("magnet:?")) {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
val intent = Intent(context, TorrentService::class.java).apply {
|
||||||
flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP
|
putExtra("EXTRA_MAGNET_URI", url)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onPageStartCallback?.invoke(url)
|
onPageStartCallback?.invoke(url)
|
||||||
}
|
}
|
||||||
@ -604,6 +615,20 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getFilterF() = String(Base64.decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", Base64.DEFAULT))
|
private fun getFilterF() = String(Base64.decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", Base64.DEFAULT))
|
||||||
private fun Context.toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
|
var toast : Toast? = null
|
||||||
|
private fun Context.toast(msg: String) {
|
||||||
|
if (toast==null) {
|
||||||
|
toast = Toast(this)
|
||||||
|
toast?.duration = Toast.LENGTH_SHORT
|
||||||
|
val biggerText = SpannableStringBuilder(msg)
|
||||||
|
biggerText.setSpan(RelativeSizeSpan(1.6f), 0, msg.length, 0)
|
||||||
|
val view: View = inflate(this, R.layout.simple_toast, null)
|
||||||
|
view.findViewById<TextView>(R.id.text).text = biggerText
|
||||||
|
toast?.view = view
|
||||||
|
}
|
||||||
|
toast?.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
|
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
|
||||||
}
|
}
|
||||||
@ -343,10 +343,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleBackPress()
|
handleBackPress()
|
||||||
updateLocationService()
|
|
||||||
|
|
||||||
|
|
||||||
// showContents(binding.feeds.id)
|
|
||||||
binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent ->
|
binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent ->
|
||||||
if (binding.floatingActionMenu.isOpened) {
|
if (binding.floatingActionMenu.isOpened) {
|
||||||
binding.floatingActionMenu.close(true)
|
binding.floatingActionMenu.close(true)
|
||||||
@ -431,12 +428,24 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
.replace(R.id.fragment_container, TokiFragment.newInstancePerplexity())
|
.replace(R.id.fragment_container, TokiFragment.newInstancePerplexity())
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
R.id.zota ->{
|
R.id.zzalbang ->{
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragment_container, BookmarkPagerFragment())
|
.replace(R.id.fragment_container, BookmarkPagerFragment())
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.btn_x ->{
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment_container, TokiFragment.newInstanceX())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.btn_torrent ->{
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment_container, TorrentListFragment())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
R.id.close ->{
|
R.id.close ->{
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
|
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
package bums.lunatic.launcher.home
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import bums.lunatic.launcher.R
|
||||||
|
import bums.lunatic.launcher.home.adapters.TorrentListAdapter
|
||||||
|
import bums.lunatic.launcher.workers.TorrentService
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class TorrentListFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var adapter: TorrentListAdapter
|
||||||
|
private var torrentService: TorrentService? = null
|
||||||
|
private var isBound = false
|
||||||
|
|
||||||
|
// 서비스 연결 콜백
|
||||||
|
private val connection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
val binder = service as TorrentService.TorrentBinder
|
||||||
|
torrentService = binder.getService()
|
||||||
|
isBound = true
|
||||||
|
observeTorrentTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||||
|
isBound = false
|
||||||
|
torrentService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_torrent_list, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerViewTorrents)
|
||||||
|
|
||||||
|
adapter = TorrentListAdapter(
|
||||||
|
onPauseResumeClick = { task ->
|
||||||
|
if (task.isPaused) {
|
||||||
|
torrentService?.resumeTorrent(task.infoHash)
|
||||||
|
} else {
|
||||||
|
torrentService?.pauseTorrent(task.infoHash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancelClick = { task ->
|
||||||
|
// UI에서 취소 시 파일까지 삭제할지 여부는 두 번째 파라미터로 결정합니다. (예: true)
|
||||||
|
torrentService?.removeTorrent(task.infoHash, deleteFile = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// 프래그먼트가 보일 때 서비스 바인딩
|
||||||
|
Intent(requireContext(), TorrentService::class.java).also { intent ->
|
||||||
|
requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
// 프래그먼트가 숨겨질 때 바인딩 해제
|
||||||
|
if (isBound) {
|
||||||
|
requireContext().unbindService(connection)
|
||||||
|
isBound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeTorrentTasks() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// 바인딩된 서비스의 Flow를 실시간 수집
|
||||||
|
torrentService?.torrentTasks?.collectLatest { tasks ->
|
||||||
|
adapter.submitList(tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// kotlin/bums/lunatic/launcher/home/adapters/TorrentListAdapter.kt
|
||||||
|
package bums.lunatic.launcher.home.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import bums.lunatic.launcher.R
|
||||||
|
import bums.lunatic.launcher.workers.TorrentTask
|
||||||
|
|
||||||
|
class TorrentListAdapter(
|
||||||
|
private val onPauseResumeClick: (TorrentTask) -> Unit,
|
||||||
|
private val onCancelClick: (TorrentTask) -> Unit
|
||||||
|
) : RecyclerView.Adapter<TorrentListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var tasks = listOf<TorrentTask>()
|
||||||
|
|
||||||
|
fun submitList(newTasks: List<TorrentTask>) {
|
||||||
|
tasks = newTasks
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val tvName: TextView = view.findViewById(R.id.tvName) // 레이아웃에 맞게 수정 필요
|
||||||
|
val tvProgress: TextView = view.findViewById(R.id.tvProgress)
|
||||||
|
val progressBar: ProgressBar = view.findViewById(R.id.progressBar)
|
||||||
|
val btnPauseResume: ImageButton = view.findViewById(R.id.btnPauseResume)
|
||||||
|
val btnCancel: ImageButton = view.findViewById(R.id.btnCancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
// res/layout/item_torrent_task.xml 을 만들어 사용하세요
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_torrent_task, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val task = tasks[position]
|
||||||
|
holder.tvName.text = task.name
|
||||||
|
holder.progressBar.progress = task.progress.toInt()
|
||||||
|
holder.tvProgress.text = String.format("%.1f%%", task.progress)
|
||||||
|
|
||||||
|
holder.btnPauseResume.setImageResource(
|
||||||
|
if (task.isPaused) android.R.drawable.ic_media_play else android.R.drawable.ic_media_pause
|
||||||
|
)
|
||||||
|
|
||||||
|
holder.btnPauseResume.setOnClickListener { onPauseResumeClick(task) }
|
||||||
|
holder.btnCancel.setOnClickListener { onCancelClick(task) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = tasks.size
|
||||||
|
}
|
||||||
@ -149,14 +149,14 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newInstanceWebtoons(): TokiFragment = TokiFragment().apply {
|
fun newInstanceX(): TokiFragment = TokiFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putString(ARG_TYPE, "webtoon")
|
putString(ARG_TYPE, "X")
|
||||||
putInt(ARG_LAST_NUM, 468)
|
putInt(ARG_LAST_NUM, 143)
|
||||||
putString(ARG_NAME, "newtoki")
|
putString(ARG_NAME, "x")
|
||||||
putString(ARG_DOT, "com")
|
putString(ARG_DOT, "com")
|
||||||
putBoolean(ARG_USE_NUM_URL, true)
|
putBoolean(ARG_USE_NUM_URL, false)
|
||||||
putBoolean(ARG_ENABLE_GESTURE, true)
|
putBoolean(ARG_ENABLE_GESTURE, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +204,17 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
|||||||
putBoolean(ARG_ENABLE_GESTURE, true)
|
putBoolean(ARG_ENABLE_GESTURE, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newInstanceWebtoons(): TokiFragment = TokiFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_TYPE, "webtoon")
|
||||||
|
putInt(ARG_LAST_NUM, 468)
|
||||||
|
putString(ARG_NAME, "newtoki")
|
||||||
|
putString(ARG_DOT, "com")
|
||||||
|
putBoolean(ARG_USE_NUM_URL, true)
|
||||||
|
putBoolean(ARG_ENABLE_GESTURE, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Unified Gesture Implementation ---
|
// --- Unified Gesture Implementation ---
|
||||||
|
|||||||
@ -0,0 +1,407 @@
|
|||||||
|
package bums.lunatic.launcher.workers
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.frostwire.jlibtorrent.*
|
||||||
|
import com.frostwire.jlibtorrent.alerts.Alert
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AlertType
|
||||||
|
import com.frostwire.jlibtorrent.alerts.TorrentFinishedAlert
|
||||||
|
import java.io.File
|
||||||
|
import com.frostwire.jlibtorrent.alerts.*
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.*
|
||||||
|
import android.os.*
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import bums.lunatic.launcher.utils.Blog
|
||||||
|
import com.frostwire.jlibtorrent.swig.error_code
|
||||||
|
import com.frostwire.jlibtorrent.swig.libtorrent
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.NonCancellable.isActive
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class TorrentTask(
|
||||||
|
val infoHash: String,
|
||||||
|
val name: String,
|
||||||
|
val progress: Float, // 0.0 ~ 100.0
|
||||||
|
val isPaused: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
class TorrentService : Service() {
|
||||||
|
|
||||||
|
private lateinit var session: SessionManager
|
||||||
|
private val binder = TorrentBinder()
|
||||||
|
|
||||||
|
// 경로 설정
|
||||||
|
private val tempDir by lazy { File(getExternalFilesDir(null), "temp_torrents").apply { mkdirs() } }
|
||||||
|
private val resumeDir by lazy { File(getExternalFilesDir(null), "resume_data").apply { mkdirs() } }
|
||||||
|
|
||||||
|
inner class TorrentBinder : Binder() {
|
||||||
|
fun getService(): TorrentService = this@TorrentService
|
||||||
|
}
|
||||||
|
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
// UI에서 관찰할 상태 Flow
|
||||||
|
private val _torrentTasks = MutableStateFlow<List<TorrentTask>>(emptyList())
|
||||||
|
val torrentTasks: StateFlow<List<TorrentTask>> = _torrentTasks
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
startForegroundService()
|
||||||
|
initLibTorrent()
|
||||||
|
startPolling() // 폴링 시작
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPolling() {
|
||||||
|
serviceScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
// 1. SessionManager에서 SWIG 원시 객체(vector)로 핸들 목록을 가져옵니다.
|
||||||
|
val vector = session.swig().get_torrents()
|
||||||
|
val tasks = mutableListOf<TorrentTask>()
|
||||||
|
|
||||||
|
// 2. 각 핸들(TorrentHandle)을 순회하며 데이터를 추출합니다.
|
||||||
|
for (i in 0 until vector.size.toInt()) {
|
||||||
|
val swigHandle = vector.get(i)
|
||||||
|
val handle = com.frostwire.jlibtorrent.TorrentHandle(swigHandle)
|
||||||
|
|
||||||
|
// 핸들이 유효한지 확인
|
||||||
|
if (!handle.isValid) continue
|
||||||
|
|
||||||
|
val status = handle.status()
|
||||||
|
val hashStr = status.infoHash().toString()
|
||||||
|
var rawName = status.name()
|
||||||
|
|
||||||
|
// 💡 핵심: TorrentHandle 객체에서 torrentFile()을 호출하여 TorrentInfo를 가져옵니다.
|
||||||
|
if (status.hasMetadata()) {
|
||||||
|
val torrentInfo = handle.torrentFile()
|
||||||
|
// torrentInfo가 null이 아니고 유효하다면 진짜 이름을 추출합니다.
|
||||||
|
if (torrentInfo != null && torrentInfo.isValid) {
|
||||||
|
val realName = torrentInfo.name()
|
||||||
|
if (!realName.isNullOrEmpty()) {
|
||||||
|
rawName = realName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val displayName = if (rawName.isNullOrEmpty() || rawName == hashStr) {
|
||||||
|
if (status.hasMetadata()) {
|
||||||
|
"파일 정보 분석 중..."
|
||||||
|
} else {
|
||||||
|
"메타데이터 수신 중... (${hashStr.take(6)})"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rawName
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.add(
|
||||||
|
TorrentTask(
|
||||||
|
infoHash = hashStr,
|
||||||
|
name = displayName,
|
||||||
|
progress = status.progress() * 100f,
|
||||||
|
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_torrentTasks.value = tasks
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
serviceScope.cancel() // 서비스 종료 시 코루틴 정리
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
// 인텐트에서 마그넷 주소를 꺼냄
|
||||||
|
val uriString = intent?.getStringExtra("EXTRA_MAGNET_URI") ?: ""
|
||||||
|
Blog.LOGE("uriString >>> $uriString")
|
||||||
|
if (uriString.startsWith("magnet:?")) {
|
||||||
|
var fixedUri = uriString
|
||||||
|
if (fixedUri.contains("xt=urn:") && !fixedUri.contains("xt=urn:btih:")) {
|
||||||
|
fixedUri = fixedUri.replace("xt=urn:", "xt=urn:btih:")
|
||||||
|
println("TorrentService: 누락된 btih: 를 추가하여 마그넷 주소를 수정했습니다 -> $fixedUri")
|
||||||
|
}
|
||||||
|
addMagnet(fixedUri)
|
||||||
|
}
|
||||||
|
// 2. .torrent 파일을 가리키는 일반 HTTP 웹 링크인 경우
|
||||||
|
else if (uriString.startsWith("http://") || uriString.startsWith("https://")) {
|
||||||
|
println("TorrentService: 마그넷이 아닌 웹 URL이 전달되었습니다 -> $uriString")
|
||||||
|
// 여기서 바로 addMagnet을 호출하면 안 됩니다!
|
||||||
|
// downloadTorrentFileFromWeb(uriString) // 별도의 파일 다운로드 로직 필요
|
||||||
|
}
|
||||||
|
// 3. 알 수 없는 형식인 경우
|
||||||
|
else {
|
||||||
|
println("TorrentService: 잘못된 형식의 URI입니다 -> $uriString")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 서비스가 강제 종료되어도 시스템이 다시 살려내도록 START_STICKY 반환
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 기존 제어 기능들 아래에 추가 ---
|
||||||
|
fun resumeTorrent(infoHash: String) {
|
||||||
|
session.find(Sha1Hash(infoHash))?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initLibTorrent() {
|
||||||
|
session = SessionManager()
|
||||||
|
|
||||||
|
session.addListener(object : AlertListener {
|
||||||
|
override fun types(): IntArray? = intArrayOf(
|
||||||
|
AlertType.TORRENT_FINISHED.swig(),
|
||||||
|
AlertType.METADATA_RECEIVED.swig(),
|
||||||
|
AlertType.SAVE_RESUME_DATA.swig()
|
||||||
|
)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
override fun alert(alert: Alert<*>) {
|
||||||
|
Blog.LOGE("alert ${alert.type()}")
|
||||||
|
when (alert.type()) {
|
||||||
|
AlertType.TORRENT_FINISHED -> {
|
||||||
|
val ta = alert as TorrentFinishedAlert
|
||||||
|
moveToPublicDownload(ta.handle())
|
||||||
|
}
|
||||||
|
AlertType.METADATA_RECEIVED -> {
|
||||||
|
val ma = alert as MetadataReceivedAlert
|
||||||
|
ma.handle().saveResumeData()
|
||||||
|
}
|
||||||
|
AlertType.SAVE_RESUME_DATA -> {
|
||||||
|
val ra = alert as SaveResumeDataAlert
|
||||||
|
saveResumeFile(ra.handle().infoHash().toString(), ra.params())
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
session.start()
|
||||||
|
restoreExistingDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 마그넷 추가 */
|
||||||
|
fun addMagnet(magnetUri: String) {
|
||||||
|
try {
|
||||||
|
val error = error_code()
|
||||||
|
|
||||||
|
// 1. SessionManager 대신 libtorrent 원시 함수로 마그넷 파싱 시도
|
||||||
|
val swigParams = libtorrent.parse_magnet_uri(magnetUri, error)
|
||||||
|
|
||||||
|
if (error.value() != 0) {
|
||||||
|
// 파싱 실패 시 어떤 이유로 실패했는지 정확한 에러 메시지 출력
|
||||||
|
println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 파싱 성공 시, 다운로드 경로 설정
|
||||||
|
swigParams.setSave_path(tempDir.absolutePath)
|
||||||
|
|
||||||
|
// 3. 원시 세션에 비동기로 토렌트 추가
|
||||||
|
session.swig().async_add_torrent(swigParams)
|
||||||
|
|
||||||
|
println("TorrentService: 마그넷 추가 성공!")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 상태 리스트 반환 */
|
||||||
|
fun getStatusList(): List<TorrentStatus> {
|
||||||
|
// SessionManager 대신 swig 원시 객체(C++ 레벨)에서 직접 벡터를 가져옵니다.
|
||||||
|
val vector = session.swig().get_torrents()
|
||||||
|
val list = mutableListOf<TorrentStatus>()
|
||||||
|
|
||||||
|
// SWIG vector size는 Long 타입이므로 toInt()로 캐스팅이 필요합니다.
|
||||||
|
for (i in 0 until vector.size.toInt()) {
|
||||||
|
val handle = com.frostwire.jlibtorrent.TorrentHandle(vector.get(i))
|
||||||
|
list.add(handle.status())
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 제어 기능 */
|
||||||
|
fun pauseTorrent(infoHash: String) {
|
||||||
|
session.find(Sha1Hash(infoHash))?.pause()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTorrent(infoHash: String, deleteFile: Boolean) {
|
||||||
|
val handle = session.find(Sha1Hash(infoHash))
|
||||||
|
if (handle != null) {
|
||||||
|
// 1.2.x 버전의 제거 로직
|
||||||
|
session.remove(handle)
|
||||||
|
if (deleteFile) {
|
||||||
|
// 파일 삭제는 수동으로 처리하거나 handle의 옵션을 확인해야 합니다.
|
||||||
|
File(tempDir, handle.status().name()).deleteRecursively()
|
||||||
|
}
|
||||||
|
File(resumeDir, "$infoHash.resume").delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun moveToPublicDownload(handle: TorrentHandle) {
|
||||||
|
try {
|
||||||
|
val status = handle.status()
|
||||||
|
val infoHash = status.infoHash().toString() // 💡 리쥼 파일 삭제용 해시값 미리 저장
|
||||||
|
var torrentName = status.name()
|
||||||
|
|
||||||
|
// 💡 날카로운 지적 반영: 메타데이터(TorrentInfo)에서 진짜 파일/폴더명을 확실하게 가져옵니다!
|
||||||
|
if (status.hasMetadata()) {
|
||||||
|
val torrentInfo = handle.torrentFile()
|
||||||
|
if (torrentInfo != null && torrentInfo.isValid) {
|
||||||
|
val realName = torrentInfo.name()
|
||||||
|
if (!realName.isNullOrEmpty()) {
|
||||||
|
torrentName = realName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진짜 이름도 없고 해시값과 동일하다면 비정상 상태로 간주
|
||||||
|
if (torrentName.isNullOrEmpty() || torrentName == status.infoHash().toString()) {
|
||||||
|
println("TorrentService: 이동할 유효한 파일/폴더 이름을 찾지 못했습니다.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진짜 이름으로 임시 폴더 내의 실제 경로를 찾음
|
||||||
|
val sourcePath = File(tempDir, torrentName)
|
||||||
|
if (!sourcePath.exists()) {
|
||||||
|
println("TorrentService: 원본 파일/폴더가 존재하지 않습니다 -> ${sourcePath.absolutePath}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 단일 파일인 경우
|
||||||
|
if (sourcePath.isFile) {
|
||||||
|
copySingleFileToDownloads(sourcePath, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
// 2. 다중 파일(폴더)인 경우
|
||||||
|
else if (sourcePath.isDirectory) {
|
||||||
|
sourcePath.walkTopDown().filter { it.isFile }.forEach { file ->
|
||||||
|
val relativeSubPath = file.parentFile?.absolutePath?.substringAfter(sourcePath.absolutePath) ?: ""
|
||||||
|
val destRelativePath = "${Environment.DIRECTORY_DOWNLOADS}/$torrentName$relativeSubPath"
|
||||||
|
|
||||||
|
copySingleFileToDownloads(file, destRelativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 복사가 모두 끝난 후 임시 파일(폴더) 삭제 및 세션에서 제거
|
||||||
|
sourcePath.deleteRecursively()
|
||||||
|
session.remove(handle)
|
||||||
|
|
||||||
|
val resumeFile = File(resumeDir, "$infoHash.resume")
|
||||||
|
if (resumeFile.exists()) {
|
||||||
|
resumeFile.delete()
|
||||||
|
println("TorrentService: 리쥼 파일 삭제 완료 ($infoHash.resume)")
|
||||||
|
}
|
||||||
|
|
||||||
|
println("TorrentService: 다운로드 완료 및 Public 폴더 이동 성공 ($torrentName)")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
println("TorrentService: 파일 이동 중 에러 발생 - ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun copySingleFileToDownloads(sourceFile: File, destRelativePath: String) {
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, sourceFile.name)
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, destRelativePath)
|
||||||
|
// 파일을 복사하는 동안 다른 앱이 접근하지 못하도록 IS_PENDING 설정
|
||||||
|
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
uri?.let { destUri ->
|
||||||
|
contentResolver.openOutputStream(destUri)?.use { output ->
|
||||||
|
sourceFile.inputStream().use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 복사 완료 후 IS_PENDING 해제
|
||||||
|
contentValues.clear()
|
||||||
|
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||||
|
contentResolver.update(destUri, contentValues, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveResumeFile(hash: String, params: AddTorrentParams) {
|
||||||
|
try {
|
||||||
|
val file = File(resumeDir, "$hash.resume")
|
||||||
|
|
||||||
|
// 1.2.x의 bencode() 대신 2.0.x 규격의 버퍼(buf_ex) 쓰기 함수 사용
|
||||||
|
val byteVector = libtorrent.write_resume_data_buf_ex(params.swig())
|
||||||
|
val data = Vectors.byte_vector2bytes(byteVector)
|
||||||
|
|
||||||
|
file.writeBytes(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExistingDownloads() {
|
||||||
|
resumeDir.listFiles()?.filter { it.extension == "resume" }?.forEach { file ->
|
||||||
|
try {
|
||||||
|
val data = file.readBytes()
|
||||||
|
|
||||||
|
// 1. 바이트 배열을 SWIG byte_vector로 변환
|
||||||
|
val byteVector = Vectors.bytes2byte_vector(data)
|
||||||
|
val error = error_code()
|
||||||
|
|
||||||
|
// 2. 올려주신 클래스에 있는 read_resume_data_ex 사용!
|
||||||
|
val swigParams = libtorrent.read_resume_data_ex(byteVector, error)
|
||||||
|
|
||||||
|
if (error.value() != 0) {
|
||||||
|
println("Resume data error: ${error.message()}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 다운로드 경로 재지정
|
||||||
|
// (SWIG는 스네이크 케이스 변수를 카멜 케이스 setter로 자동 변환합니다)
|
||||||
|
swigParams.setSave_path(tempDir.absolutePath)
|
||||||
|
|
||||||
|
// 4. SessionManager의 C++ 원시 핸들에 직접 접근하여 토렌트 추가 (비동기 권장)
|
||||||
|
session.swig().async_add_torrent(swigParams)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder = binder
|
||||||
|
|
||||||
|
private fun startForegroundService() {
|
||||||
|
val channelId = "torrent_channel"
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(channelId, "Torrent Service", NotificationManager.IMPORTANCE_LOW)
|
||||||
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
val notification = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
.setContentTitle("Torrent Manager Active")
|
||||||
|
.build()
|
||||||
|
startForeground(101, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -72,7 +72,8 @@ object WorkersDb {
|
|||||||
WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
|
WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
|
||||||
LocationLog::class,
|
LocationLog::class,
|
||||||
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
|
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
|
||||||
WidgetData::class,AppUsageLog::class
|
WidgetData::class,AppUsageLog::class,
|
||||||
|
|
||||||
)
|
)
|
||||||
//,UserActionModel::class
|
//,UserActionModel::class
|
||||||
|
|
||||||
|
|||||||
6
app/src/main/res/color/bottom_option.xml
Normal file
6
app/src/main/res/color/bottom_option.xml
Normal file
@ -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/finestSilver" />
|
||||||
|
</selector>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
<corners android:radius="10dp"/>
|
<corners android:radius="5dp"/>
|
||||||
<solid android:color="#44ffffff"/>
|
<padding android:top="5dp" android:bottom="5dp" android:left="5dp" android:right="5dp"/>
|
||||||
<stroke android:color="@color/white" android:width="1dp"/>
|
<solid android:color="#7111"/>
|
||||||
|
<stroke android:color="#5fff" android:width="0.5dp"/>
|
||||||
</shape>
|
</shape>
|
||||||
33
app/src/main/res/layout/fragment_torrent_list.xml
Normal file
33
app/src/main/res/layout/fragment_torrent_list.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="#E0E0E0"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="다운로드 목록"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingBottom="16dp"/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerViewTorrents"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
77
app/src/main/res/layout/item_torrent_task.xml
Normal file
77
app/src/main/res/layout/item_torrent_task.xml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="Ubuntu-22.04-desktop-amd64.iso"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnPauseResume"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="45.2%"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvName"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:max="100"
|
||||||
|
tools:progress="45"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvName"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/tvProgress"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnPauseResume"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/tvProgress"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnPauseResume"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_media_pause"
|
||||||
|
app:tint="?android:attr/textColorPrimary"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnCancel"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="일시정지/재개" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
|
app:tint="#D32F2F"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:contentDescription="다운로드 취소" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -144,14 +144,30 @@
|
|||||||
android:layout_height="20dp"/>
|
android:layout_height="20dp"/>
|
||||||
<bums.lunatic.launcher.view.FloatingActionButton
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
app:fab_label="짤방"
|
app:fab_label="짤방"
|
||||||
android:id="@+id/zota"
|
android:id="@+id/zzalbang"
|
||||||
app:fab_showShadow="true"
|
app:fab_showShadow="true"
|
||||||
app:fab_size="mini"
|
app:fab_size="mini"
|
||||||
android:onClick="floatClick"
|
android:onClick="floatClick"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="20dp"/>
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
|
app:fab_label="X"
|
||||||
|
android:id="@+id/btn_x"
|
||||||
|
app:fab_showShadow="true"
|
||||||
|
app:fab_size="mini"
|
||||||
|
android:onClick="floatClick"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
|
app:fab_label="torrent"
|
||||||
|
android:id="@+id/btn_torrent"
|
||||||
|
app:fab_showShadow="true"
|
||||||
|
app:fab_size="mini"
|
||||||
|
android:onClick="floatClick"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
<bums.lunatic.launcher.view.FloatingActionButton
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
app:fab_label="close"
|
app:fab_label="close"
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:gravity="left"
|
android:gravity="left"
|
||||||
android:id="@+id/text"
|
android:id="@+id/text"
|
||||||
android:textColor="@color/black"
|
android:textSize="@dimen/_12sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
<item name="android:layout_width">wrap_content</item>
|
<item name="android:layout_width">wrap_content</item>
|
||||||
<item name="android:layout_height">35dp</item>
|
<item name="android:layout_height">35dp</item>
|
||||||
<item name="android:gravity">center</item>
|
<item name="android:gravity">center</item>
|
||||||
<item name="android:textColor">@color/finestSilver</item>
|
<item name="android:textColor">@color/tabs_black</item>
|
||||||
<item name="android:visibility">visible</item>
|
<item name="android:visibility">visible</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="asdda" parent="Widget.Material3.Button.OutlinedButton">
|
<style name="asdda" parent="Widget.Material3.Button.OutlinedButton">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user