Initial commit: Add webtoon viewer
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
18
.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-18T17:19:25.515939100Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=7006009d" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
329
.idea/other.xml
generated
Normal file
@@ -0,0 +1,329 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
58
app/build.gradle.kts
Normal file
@@ -0,0 +1,58 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.webtoonviewer"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.webtoonviewer"
|
||||
minSdk = 31
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
implementation(libs.smbj)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation (libs.gson)
|
||||
implementation (libs.androidx.room.runtime)
|
||||
kapt ("androidx.room:room-compiler:2.6.1")
|
||||
implementation ("androidx.room:room-ktx:2.6.1")
|
||||
implementation ("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation (libs.github.glide)
|
||||
annotationProcessor (libs.compiler)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.webtoonviewer
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.webtoonviewer", appContext.packageName)
|
||||
}
|
||||
}
|
||||
48
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Base.Theme.WebtoonViewer"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Chapter Activity -->
|
||||
<activity android:name="com.webtoonviewer.ui.EpisodesActivity" />
|
||||
|
||||
<!-- Image Activity -->
|
||||
<activity android:name="com.webtoonviewer.ui.ImagesActivity" />
|
||||
|
||||
<!-- Description Activity -->
|
||||
<activity android:name="com.webtoonviewer.ui.DescriptionActivity" />
|
||||
|
||||
<!-- Directory Activity -->
|
||||
<activity android:name="com.webtoonviewer.ui.DirectoryActivity" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/115.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
app/src/main/assets/116.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
app/src/main/assets/142.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/assets/152.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
app/src/main/assets/167.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/assets/200.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
app/src/main/assets/222.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
app/src/main/assets/230.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
app/src/main/assets/231.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
app/src/main/assets/233.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
app/src/main/assets/247.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
app/src/main/assets/255.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
app/src/main/assets/303.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
app/src/main/assets/322.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/assets/330.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app/src/main/assets/368.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
app/src/main/assets/399.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/assets/41.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
app/src/main/assets/421.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
app/src/main/assets/424.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
app/src/main/assets/429.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
app/src/main/assets/433.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
app/src/main/assets/450.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
app/src/main/assets/484.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
app/src/main/assets/489.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/assets/49.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
app/src/main/assets/516.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
app/src/main/assets/53.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
app/src/main/assets/545.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
app/src/main/assets/547.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
app/src/main/assets/587.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
app/src/main/assets/591.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
app/src/main/assets/595.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/assets/617.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
app/src/main/assets/711.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
app/src/main/assets/736.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
app/src/main/assets/784.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
app/src/main/assets/787.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
app/src/main/assets/810.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
app/src/main/assets/814.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
app/src/main/assets/862.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
app/src/main/assets/academys-undercover-professor.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app/src/main/assets/childhood-friend-complex.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
app/src/main/assets/couple-breaker.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
app/src/main/assets/duzhe.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/assets/estatedeveloper.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app/src/main/assets/just-twilight.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
app/src/main/assets/like-mother-like-daughter.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/assets/mad-demon.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/assets/myst-might-mayhem.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
app/src/main/assets/night-of-shadows.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
app/src/main/assets/only-hope.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
app/src/main/assets/surviving-the-game-as-a-barbarian.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
app/src/main/assets/the-spark-in-your-eyes.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
app/src/main/assets/trap.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
app/src/main/assets/youth-of-revelation.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
21
app/src/main/java/com/webtoonviewer/App.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.webtoonviewer
|
||||
|
||||
import android.app.Application
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.webtoonviewer.work.UpdateBooks
|
||||
|
||||
class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val update = true
|
||||
|
||||
|
||||
if (update) {
|
||||
val workRequest = OneTimeWorkRequest.Builder(UpdateBooks::class.java).build()
|
||||
WorkManager.getInstance(this).enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/com/webtoonviewer/db/AppDatabase.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.webtoonviewer.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
@Database(entities = [Book::class], version = 8)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun bookDao(): BookDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: AppDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): AppDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
"app_database"
|
||||
).addMigrations(MIGRATION_1_2).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_1_2 = object : Migration(7, 8) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// // 1. 创建临时表
|
||||
database.execSQL(
|
||||
"CREATE TABLE books_temp (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
|
||||
"title TEXT NOT NULL, " +
|
||||
"author TEXT NOT NULL, " +
|
||||
"description TEXT NOT NULL, " +
|
||||
"tag TEXT NOT NULL, " +
|
||||
"img TEXT NOT NULL, " +
|
||||
"chapters TEXT NOT NULL, " +
|
||||
"chapterSum INTEGER NOT NULL, " +
|
||||
"bookmarkChapter TEXT NOT NULL, " +
|
||||
"bookmarkPosition INTEGER NOT NULL, " +
|
||||
"bookmarkTopOffset INTEGER NOT NULL, " +
|
||||
"isLocal INTEGER NOT NULL )"
|
||||
)
|
||||
|
||||
// 2. 从旧表复制数据到新表
|
||||
database.execSQL("""
|
||||
INSERT INTO books_temp (id, title, author, description, tag, img, chapters, chapterSum, bookmarkChapter, bookmarkPosition, bookmarkTopOffset, isLocal)
|
||||
SELECT id, title, author, description, tag, img, chapters, chapterSum, bookmarkChapter, bookmarkPosition, bookmarkTopOffset, '0' FROM books
|
||||
""")
|
||||
|
||||
// 3. 删除旧表
|
||||
database.execSQL("DROP TABLE books")
|
||||
|
||||
// 4. 将新表重命名为旧表
|
||||
database.execSQL("ALTER TABLE books_temp RENAME TO books")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/com/webtoonviewer/db/Book.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.webtoonviewer.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
@Entity(tableName = "books")
|
||||
data class Book(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "author") val author: String,
|
||||
@ColumnInfo(name = "description") val description: String,
|
||||
@ColumnInfo(name = "tag") val tag: String,
|
||||
@ColumnInfo(name = "img") val img: String,
|
||||
@ColumnInfo(name = "chapters") val chapters: MutableList<String>,
|
||||
@ColumnInfo(name = "chapterSum") val chapterSum: Int,
|
||||
@ColumnInfo(name = "bookmarkChapter") var bookmarkChapter: String,
|
||||
@ColumnInfo(name = "bookmarkPosition") var bookmarkPosition: Int,
|
||||
@ColumnInfo(name = "bookmarkTopOffset") var bookmarkTopOffset: Int,
|
||||
@ColumnInfo(name = "isLocal") var isLocal: Int,
|
||||
) {
|
||||
constructor(id: Int, title: String, author: String, description: String, tag: String, img: String, isLocal: Int) : this(
|
||||
id = id,
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
tag = tag,
|
||||
img = img,
|
||||
chapters = mutableListOf(),
|
||||
chapterSum = 0,
|
||||
bookmarkChapter = "",
|
||||
bookmarkPosition = 0,
|
||||
bookmarkTopOffset = 0,
|
||||
isLocal = isLocal
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun fromString(value: String): MutableList<String> {
|
||||
val listType = object : TypeToken<List<String>>() {}.type
|
||||
return Gson().fromJson(value, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromList(list: MutableList<String>): String {
|
||||
return Gson().toJson(list)
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/com/webtoonviewer/db/BookDao.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.webtoonviewer.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface BookDao {
|
||||
|
||||
@Query("SELECT * FROM books")
|
||||
suspend fun getAllBooks(): List<Book>
|
||||
|
||||
@Query("SELECT title FROM books")
|
||||
suspend fun getAllTitles(): List<String>
|
||||
|
||||
@Query("SELECT * FROM books WHERE title = :title")
|
||||
suspend fun getBookByTitle(title: String): Book?
|
||||
|
||||
@Query("SELECT * FROM books WHERE id = :bookId")
|
||||
suspend fun getBookById(bookId: Int): Book?
|
||||
|
||||
@Query("UPDATE books SET title = :title WHERE id = :bookId")
|
||||
suspend fun updateTitleById(bookId: Int, title: String)
|
||||
|
||||
@Query("UPDATE books SET author = :author, tag = :tag, description = :description, img = :img WHERE id = :bookId")
|
||||
suspend fun updateInformationById(bookId: Int, author: String, tag: String, description: String, img: String)
|
||||
|
||||
@Query("UPDATE books SET tag = :tag WHERE id = :bookId")
|
||||
suspend fun updateTagById(bookId: Int, tag: String)
|
||||
|
||||
@Query("UPDATE books SET description = :description WHERE id = :bookId")
|
||||
suspend fun updateDescriptionById(bookId: Int, description: String)
|
||||
|
||||
@Query("UPDATE books SET chapters = :chapters, chapterSum = :chapterSum WHERE id = :bookId")
|
||||
suspend fun updateChaptersById(bookId: Int, chapters: List<String>, chapterSum: Int)
|
||||
|
||||
@Query("UPDATE books SET bookmarkChapter = :bookmarkChapter, bookmarkPosition = :bookmarkPosition, bookmarkTopOffset = :bookmarkTopOffset WHERE id = :bookId")
|
||||
suspend fun updateBookmarkById(bookId: Int, bookmarkChapter: String, bookmarkPosition: Int, bookmarkTopOffset: Int)
|
||||
|
||||
@Query("UPDATE books SET isLocal = :isLocal WHERE id = :bookId")
|
||||
suspend fun updateIsLocalById(bookId: Int, isLocal: Int)
|
||||
|
||||
@Query("DELETE FROM books WHERE id = :bookId")
|
||||
suspend fun deleteBookById(bookId: Int)
|
||||
// 添加新书籍
|
||||
@Insert
|
||||
suspend fun addBook(book: Book)
|
||||
|
||||
@Query("UPDATE sqlite_sequence SET seq = :startId WHERE name = 'books'")
|
||||
suspend fun setAutoIncrement(startId: Int)
|
||||
|
||||
//
|
||||
// @Query("SELECT * FROM sqlite_sequence WHERE name = 'books'")
|
||||
// suspend fun getAutoIncrement()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.webtoonviewer.network
|
||||
|
||||
object ConnectionData {
|
||||
const val SHARED_FOLDER: String = "Media"
|
||||
const val WEBTOON_FOLDER: String = "Webtoon"
|
||||
const val USERNAME: String = "halio"
|
||||
const val PASSWORD: String = "zhao8888"
|
||||
const val DOMAIN: String = ""
|
||||
const val HOSTNAME: String = "192.168.0.10"
|
||||
}
|
||||
201
app/src/main/java/com/webtoonviewer/network/SmbClientHelper.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package com.webtoonviewer.network
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.hierynomus.msdtyp.AccessMask
|
||||
import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation
|
||||
import com.hierynomus.mssmb2.SMB2CreateDisposition
|
||||
import com.hierynomus.mssmb2.SMB2ShareAccess
|
||||
import com.hierynomus.smbj.SMBClient
|
||||
import com.hierynomus.smbj.auth.AuthenticationContext
|
||||
import com.hierynomus.smbj.connection.Connection
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
import com.hierynomus.smbj.share.File
|
||||
import com.webtoonviewer.storage.StorageManager
|
||||
import com.webtoonviewer.utils.CommonUtils.pathJoin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class SmbClientHelper(private val context: Context) {
|
||||
private lateinit var connection: Connection
|
||||
private lateinit var session: Session
|
||||
private lateinit var share: DiskShare
|
||||
|
||||
suspend fun connect(): Boolean {
|
||||
return try {
|
||||
val client = SMBClient()
|
||||
|
||||
// 使用 withContext 切换到 IO 线程执行连接操作
|
||||
withContext(Dispatchers.IO) {
|
||||
connection = client.connect(ConnectionData.HOSTNAME)
|
||||
Log.d("SmbClientHelper", "Connected to server: ${ConnectionData.HOSTNAME}")
|
||||
|
||||
val ac = AuthenticationContext(
|
||||
ConnectionData.USERNAME, ConnectionData.PASSWORD.toCharArray(),
|
||||
ConnectionData.DOMAIN
|
||||
)
|
||||
session = connection.authenticate(ac)
|
||||
Log.d("SmbClientHelper", "Authenticated with username: ${ConnectionData.USERNAME}")
|
||||
|
||||
share = session.connectShare(ConnectionData.SHARED_FOLDER) as DiskShare
|
||||
Log.d("SmbClientHelper", "Connected to share: ${ConnectionData.SHARED_FOLDER}")
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 捕获 SMB 连接异常
|
||||
Log.e("SmbClientHelper", "Error connecting to share", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getTitlesFromShare(): List<FileIdBothDirectoryInformation> {
|
||||
return share.list(ConnectionData.WEBTOON_FOLDER)
|
||||
}
|
||||
|
||||
fun getEpisodesFormShare(title: String): List<FileIdBothDirectoryInformation> {
|
||||
val path = ConnectionData.WEBTOON_FOLDER + "/" + title
|
||||
return share.list(path)
|
||||
}
|
||||
|
||||
fun getChapterSumFromShare(title: String): Int {
|
||||
val path = ConnectionData.WEBTOON_FOLDER + "/" + title
|
||||
return share.list(path).count() - 3
|
||||
}
|
||||
|
||||
fun getInformationFile(title: String): File {
|
||||
return share.openFile(
|
||||
pathJoin(ConnectionData.WEBTOON_FOLDER, title, "information.json"),
|
||||
setOf(AccessMask.GENERIC_READ),
|
||||
null,
|
||||
SMB2ShareAccess.ALL,
|
||||
SMB2CreateDisposition.FILE_OPEN,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getImagePathsFromShare(title: String, episode: String): MutableList<String> {
|
||||
val episodeFolderPath = pathJoin(ConnectionData.WEBTOON_FOLDER, title, episode)
|
||||
return withContext(Dispatchers.IO) {
|
||||
var imageNames = mutableListOf<String>()
|
||||
for (item in share.list(episodeFolderPath)) {
|
||||
val fileInfo = item as FileIdBothDirectoryInformation
|
||||
if (fileInfo.fileName.endsWith(".jpg", true)) {
|
||||
imageNames.add(fileInfo.fileName)
|
||||
}
|
||||
}
|
||||
imageNames = imageNames.sortedBy { it.substringBefore('.').toInt() }.toMutableList()
|
||||
imageNames.map { "$episodeFolderPath/$it" }.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadImageFromShare(imagePath: String): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val file = share.openFile(
|
||||
imagePath,
|
||||
setOf(AccessMask.GENERIC_READ),
|
||||
null,
|
||||
SMB2ShareAccess.ALL,
|
||||
SMB2CreateDisposition.FILE_OPEN,
|
||||
null
|
||||
)
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val inputStream: InputStream = file.inputStream
|
||||
inputStream.copyTo(outputStream)
|
||||
file.close()
|
||||
|
||||
val imageData = outputStream.toByteArray()
|
||||
BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadWebtoon(title: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
for (episode in getEpisodesFormShare(title)) {
|
||||
if (episode.fileName != "." && episode.fileName != ".." && !episode.fileName.contains("information")) {
|
||||
val dirName = title + "/" + episode.fileName
|
||||
if (!StorageManager.isDirectoryExists(context, dirName)) {
|
||||
StorageManager.createDirectory(context, dirName)
|
||||
copyImages(getImagePathsFromShare(title, episode.fileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun copyImages(imagePaths: MutableList<String>) {
|
||||
withContext(Dispatchers.IO) {
|
||||
for (imagePath in imagePaths) {
|
||||
val file = share.openFile(
|
||||
imagePath,
|
||||
setOf(AccessMask.GENERIC_READ),
|
||||
null,
|
||||
SMB2ShareAccess.ALL,
|
||||
SMB2CreateDisposition.FILE_OPEN,
|
||||
null
|
||||
)
|
||||
|
||||
val outputFile = StorageManager.generateOutputFile(context, imagePath)
|
||||
Log.d("ithi", "copyImages: ${outputFile}")
|
||||
FileOutputStream(outputFile).use { outputStream ->
|
||||
file.inputStream.use { inputStream ->
|
||||
copyStream(inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
Log.d("SmbClientHelper", "Saved image to: ${outputFile.absolutePath}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private suspend fun copyImages(imagePaths: MutableList<String>) {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// for (imagePath in imagePaths) {
|
||||
// val file = share.openFile(
|
||||
// imagePath,
|
||||
// setOf(AccessMask.GENERIC_READ),
|
||||
// null,
|
||||
// SMB2ShareAccess.ALL,
|
||||
// SMB2CreateDisposition.FILE_OPEN,
|
||||
// null
|
||||
// )
|
||||
// val outputFile = externalStorageHelper.generateOutputFile(imagePath)
|
||||
// if (outputFile != null) {
|
||||
// FileOutputStream(outputFile).use { outputStream ->
|
||||
// file.inputStream.use { inputStream ->
|
||||
// copyStream(inputStream, outputStream)
|
||||
// }
|
||||
// }
|
||||
// Log.d("SmbClientHelper", "Saved image to: ${outputFile.absolutePath}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
|
||||
val buffer = ByteArray(1024 * 4) // 4KB buffer
|
||||
var bytesRead: Int
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
suspend fun disconnect() {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
share.close()
|
||||
session.close()
|
||||
connection.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("SmbClientHelper", "Error disconnecting", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.webtoonviewer.repository
|
||||
|
||||
data class BookInformation(
|
||||
val title: String,
|
||||
val author: String,
|
||||
val tag: String,
|
||||
val description: String,
|
||||
val thumbnail_name: String
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.webtoonviewer.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.webtoonviewer.db.AppDatabase
|
||||
import com.webtoonviewer.db.Book
|
||||
|
||||
class BookRepository(context: Context) {
|
||||
private val bookDao = AppDatabase.getDatabase(context).bookDao()
|
||||
|
||||
suspend fun getAllBookTitles(): MutableList<String> {
|
||||
return bookDao.getAllTitles().toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getAllBooks(): MutableList<Book> {
|
||||
return bookDao.getAllBooks().toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getBookByTitle(title: String): Book? {
|
||||
return bookDao.getBookByTitle(title)
|
||||
}
|
||||
|
||||
suspend fun getBookById(id: Int): Book? {
|
||||
return bookDao.getBookById(id)
|
||||
}
|
||||
|
||||
suspend fun updateTitleById(id: Int, title: String) {
|
||||
bookDao.updateTitleById(id, title)
|
||||
}
|
||||
|
||||
suspend fun updateInformationById(id: Int, info: BookInformation) {
|
||||
bookDao.updateInformationById(id, info.author, info.tag, info.description, info.thumbnail_name)
|
||||
}
|
||||
|
||||
suspend fun updateDescriptionById(id: Int, description: String) {
|
||||
bookDao.updateDescriptionById(id, description)
|
||||
}
|
||||
|
||||
suspend fun updateChaptersById(id: Int, chapters: List<String>, chapterSum: Int) {
|
||||
bookDao.updateChaptersById(id, chapters,chapterSum)
|
||||
}
|
||||
|
||||
suspend fun updateBookmarkById(id: Int, bookmarkChapter: String, bookmarkPosition: Int, bookmarkTopOffset: Int) {
|
||||
bookDao.updateBookmarkById(id, bookmarkChapter, bookmarkPosition, bookmarkTopOffset)
|
||||
}
|
||||
|
||||
suspend fun updateIsLocal(id: Int, isLocal: Int) {
|
||||
bookDao.updateIsLocalById(id, isLocal)
|
||||
}
|
||||
|
||||
suspend fun addNewBook(book: Book) {
|
||||
bookDao.addBook(book)
|
||||
}
|
||||
|
||||
suspend fun copyBook(sourceId: Int, targetId: Int) {
|
||||
val book = bookDao.getBookById(sourceId)
|
||||
if (book != null) {
|
||||
bookDao.updateTitleById(targetId, book.title)
|
||||
bookDao.updateInformationById(targetId, book.author, book.tag, book.description, book.img)
|
||||
bookDao.updateChaptersById(targetId, book.chapters, book.chapterSum)
|
||||
bookDao.updateBookmarkById(targetId, book.bookmarkChapter, book.bookmarkPosition, book.bookmarkTopOffset)
|
||||
bookDao.updateIsLocalById(targetId, book.isLocal)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteBookById(bookId: Int) {
|
||||
bookDao.deleteBookById(bookId)
|
||||
}
|
||||
|
||||
}
|
||||
126
app/src/main/java/com/webtoonviewer/storage/StorageManager.kt
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.webtoonviewer.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.webtoonviewer.ui.data.FileItem
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
object StorageManager {
|
||||
|
||||
private fun getWebtoonDir(context: Context): File {
|
||||
return File(context.filesDir, "Webtoon")
|
||||
}
|
||||
|
||||
// 检查指定目录是否存在
|
||||
fun isDirectoryExists(context: Context, dirName: String): Boolean {
|
||||
val directory = File(getWebtoonDir(context), dirName)
|
||||
return directory.exists() && directory.isDirectory
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
fun createDirectory(context: Context, dirName: String) {
|
||||
val directory = File(getWebtoonDir(context), dirName)
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs() // 创建目录及其子目录
|
||||
}
|
||||
}
|
||||
|
||||
// 删除目录及其内容
|
||||
fun deleteDirectory(context: Context, dirPath: String): Boolean {
|
||||
val directory = File(dirPath)
|
||||
return deleteRecursive(directory)
|
||||
}
|
||||
|
||||
// 递归删除文件和目录
|
||||
private fun deleteRecursive(fileOrDirectory: File): Boolean {
|
||||
if (fileOrDirectory.isDirectory) {
|
||||
fileOrDirectory.listFiles()?.forEach { child ->
|
||||
if (!deleteRecursive(child)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileOrDirectory.delete()
|
||||
}
|
||||
|
||||
// 获取目录下的文件列表
|
||||
fun getFilesAndDirectory(context: Context, dirPath: String): MutableList<FileItem> {
|
||||
val directory = if (dirPath == "") {
|
||||
context.filesDir
|
||||
} else {
|
||||
File(dirPath)
|
||||
}
|
||||
val fileItems = mutableListOf<FileItem>()
|
||||
|
||||
directory.listFiles()?.forEach { file ->
|
||||
fileItems.add(FileItem(file.name, file.isDirectory, file.absolutePath))
|
||||
}
|
||||
return fileItems
|
||||
}
|
||||
|
||||
fun getFilesAndDirectorySum(context: Context, dirPath: String): Int {
|
||||
val directory = if (dirPath == "") {
|
||||
context.filesDir
|
||||
} else {
|
||||
File(dirPath)
|
||||
}
|
||||
return directory.listFiles()?.size ?: 0
|
||||
}
|
||||
|
||||
fun getAllWebtoonTitles(context: Context): MutableList<String> {
|
||||
return getAllWebtoonTitles(getWebtoonDir(context))
|
||||
}
|
||||
|
||||
private fun getAllWebtoonTitles(dir: File): MutableList<String> {
|
||||
val titles = mutableListOf<String>()
|
||||
val items = dir.listFiles()
|
||||
if (items != null) {
|
||||
for (item in items) {
|
||||
if (item.isDirectory) {
|
||||
titles.add(item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
fun readFile(context: Context, dirName: String, fileName: String): String? {
|
||||
val directory = File(context.filesDir, dirName)
|
||||
val file = File(directory, fileName)
|
||||
return if (file.exists()) {
|
||||
file.readText()
|
||||
} else {
|
||||
null // 文件不存在时返回null
|
||||
}
|
||||
}
|
||||
|
||||
fun generateOutputFile(context: Context, imagePath: String): File {
|
||||
// imagePath: Webtoon/title/episode/image.jpg
|
||||
return File(context.filesDir, imagePath)
|
||||
}
|
||||
|
||||
fun getImagePaths(context: Context, title: String, episode: String): MutableList<File> {
|
||||
val episodeDir = File(getWebtoonDir(context), "$title/$episode")
|
||||
val imagePaths = mutableListOf<File>()
|
||||
val items = episodeDir.listFiles()
|
||||
val sum = items?.size ?: 0
|
||||
for (i in 1 until sum + 1) {
|
||||
imagePaths.add(File(episodeDir,"$i.jpg"))
|
||||
}
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
fun loadImage(imagePath: File): Bitmap? {
|
||||
return if (imagePath.exists()) {
|
||||
val inputStream = FileInputStream(imagePath)
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
112
app/src/main/java/com/webtoonviewer/ui/DescriptionActivity.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.db.Book
|
||||
import com.webtoonviewer.repository.BookInformation
|
||||
import com.webtoonviewer.repository.BookRepository
|
||||
import com.webtoonviewer.utils.StatusBar.darkStatusBar
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DescriptionActivity : AppCompatActivity() {
|
||||
private var bookRepository: BookRepository = BookRepository(this)
|
||||
private lateinit var book: Book
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_description)
|
||||
|
||||
val id = intent.getIntExtra("id", 0)
|
||||
val description = intent.getStringExtra("description")
|
||||
val scrollView: ScrollView = findViewById(R.id.scrollView)
|
||||
val textView: TextView = findViewById(R.id.textViewDescriptionPage)
|
||||
textView.text = description
|
||||
val editText: EditText = findViewById(R.id.editTextDescriptionPage)
|
||||
editText.setText(textView.text)
|
||||
val spinner: Spinner = findViewById(R.id.spinnerDescriptionPage)
|
||||
val buttonSubmit: ImageButton = findViewById(R.id.btnDescriptionPageSubmit)
|
||||
|
||||
textView.setOnLongClickListener {
|
||||
scrollView.visibility = View.GONE
|
||||
textView.visibility = View.GONE
|
||||
editText.visibility = View.VISIBLE
|
||||
spinner.visibility = View.VISIBLE
|
||||
buttonSubmit.visibility = View.VISIBLE
|
||||
true
|
||||
}
|
||||
|
||||
editText.movementMethod = ScrollingMovementMethod()
|
||||
|
||||
ArrayAdapter.createFromResource(
|
||||
this,
|
||||
R.array.tag_items, // 这里是你的字符串数组资源
|
||||
android.R.layout.simple_spinner_item
|
||||
).also { adapter ->
|
||||
// 设置下拉菜单样式
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
// 将适配器应用于Spinner
|
||||
spinner.adapter = adapter
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
book = bookRepository.getBookById(id)!!
|
||||
val tag = book.tag
|
||||
val position = getPositionFromArray(R.array.tag_items, tag)
|
||||
if (position != -1) {
|
||||
spinner.setSelection(position)
|
||||
}
|
||||
}
|
||||
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val selectedItem = parent.getItemAtPosition(position).toString()
|
||||
Log.d("ithi", "onItemSelected: $selectedItem")
|
||||
// 处理选中的项
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||
// 处理未选择任何项的情况
|
||||
}
|
||||
}
|
||||
|
||||
buttonSubmit.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val info = BookInformation(
|
||||
book.title,
|
||||
book.author,
|
||||
spinner.selectedItem.toString(),
|
||||
editText.text.toString(),
|
||||
book.img)
|
||||
bookRepository.updateDescriptionById(id, editText.text.toString())
|
||||
|
||||
bookRepository.updateInformationById(id, info)
|
||||
scrollView.visibility = View.VISIBLE
|
||||
textView.visibility = View.VISIBLE
|
||||
editText.visibility = View.GONE
|
||||
spinner.visibility = View.GONE
|
||||
buttonSubmit.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val buttonClose: ImageButton = findViewById(R.id.btnDescriptionPageClose)
|
||||
buttonClose.setOnClickListener {
|
||||
finish() // 关闭当前 Activity
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPositionFromArray(arrayResId: Int, targetString: String): Int {
|
||||
val array = resources.getStringArray(arrayResId)
|
||||
return array.indexOf(targetString)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.utils.SharedPreferencesHelper
|
||||
|
||||
class DialogAutoScrollSpeedFragment: DialogFragment() {
|
||||
private lateinit var speedPreferences: SharedPreferencesHelper
|
||||
|
||||
companion object {
|
||||
private const val ARG_PARAM = "param"
|
||||
|
||||
fun newInstance(param: String): DialogAutoScrollSpeedFragment {
|
||||
val fragment = DialogAutoScrollSpeedFragment()
|
||||
val args = Bundle().apply {
|
||||
putString(ARG_PARAM, param)
|
||||
}
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_auto_scroll_speed, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
speedPreferences = SharedPreferencesHelper(requireContext(), "auto_scroll_speed")
|
||||
|
||||
val tabLayout: TabLayout = view.findViewById(R.id.tabLayoutDialogAutoScrollSpeed)
|
||||
|
||||
// Adding tabs to TabLayout
|
||||
val tabs = listOf("1x", "2x", "3x", "4x", "5x")
|
||||
for (tab in tabs) {
|
||||
tabLayout.addTab(tabLayout.newTab().setText(tab))
|
||||
}
|
||||
|
||||
val title = arguments?.getString(ARG_PARAM) ?: ""
|
||||
val textView: TextView = view.findViewById(R.id.textViewImageDialogTitle)
|
||||
textView.text = title
|
||||
val savedTab = speedPreferences.readData(title, "1x")
|
||||
|
||||
// Set initial selected tab
|
||||
val initialTabIndex = tabs.indexOf(savedTab)
|
||||
tabLayout.getTabAt(initialTabIndex)?.select()
|
||||
|
||||
// Handle tab selection
|
||||
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
tab?.let {
|
||||
val selectedText = it.text.toString()
|
||||
speedPreferences.saveData(title, selectedText)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.ui.adapter.TagAdapter
|
||||
import com.webtoonviewer.ui.data.CheckboxItem
|
||||
import com.webtoonviewer.utils.SharedPreferencesHelper
|
||||
|
||||
class DialogMainSettingsFragment: DialogFragment() {
|
||||
private lateinit var tagsVisibilityPreferences: SharedPreferencesHelper
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_main_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
tagsVisibilityPreferences = SharedPreferencesHelper(requireContext(), "tags_visibility")
|
||||
|
||||
val checkBoxShowAll: CheckBox = view.findViewById(R.id.checkBoxDialogMainSettingsShowAll)
|
||||
checkBoxShowAll.isChecked = tagsVisibilityPreferences.getBoolean("showAll", false)
|
||||
|
||||
// Initialize RecyclerView
|
||||
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerViewDialogMainSettingsTagList)
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
val tagsItems = loadCheckboxItems()
|
||||
val tagAdapter = TagAdapter(tagsItems) { position, isChecked ->
|
||||
tagsItems[position].isChecked = isChecked
|
||||
if (!isChecked) {
|
||||
checkBoxShowAll.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.adapter = tagAdapter
|
||||
|
||||
checkBoxShowAll.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
tagAdapter.setItemsChecked()
|
||||
}
|
||||
}
|
||||
|
||||
val subMitButton: Button = view.findViewById(R.id.btnDialogMainSettingsSubmit)
|
||||
subMitButton.setOnClickListener {
|
||||
var refresh_necessary = false
|
||||
if (tagsVisibilityPreferences.getBoolean("showAll", false) != checkBoxShowAll.isChecked) {
|
||||
tagsVisibilityPreferences.putBoolean("showAll", checkBoxShowAll.isChecked)
|
||||
refresh_necessary = true
|
||||
}
|
||||
|
||||
tagsItems.forEach { item ->
|
||||
if (tagsVisibilityPreferences.getBoolean(item.text, false) != item.isChecked) {
|
||||
tagsVisibilityPreferences.putBoolean(item.text, item.isChecked)
|
||||
refresh_necessary = true
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
if (refresh_necessary) {
|
||||
(activity as? MainActivity)?.refreshActivity() // Refresh the activity
|
||||
}
|
||||
}
|
||||
|
||||
val dirButton: Button = view.findViewById(R.id.btnDialogMainSettingsDirPage)
|
||||
dirButton.setOnClickListener {
|
||||
dismiss()
|
||||
val intent = Intent(requireContext(), DirectoryActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCheckboxItems(): MutableList<CheckboxItem> {
|
||||
val tags = resources.getStringArray(R.array.tag_items)
|
||||
val items = mutableListOf<CheckboxItem>()
|
||||
for (tag in tags) {
|
||||
val isChecked = tagsVisibilityPreferences.getBoolean(tag, false)
|
||||
items.add(CheckboxItem(tag, isChecked))
|
||||
}
|
||||
return items
|
||||
}
|
||||
}
|
||||
101
app/src/main/java/com/webtoonviewer/ui/DirectoryActivity.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.storage.StorageManager
|
||||
import com.webtoonviewer.ui.adapter.FileAdapter
|
||||
import com.webtoonviewer.ui.data.FileItem
|
||||
import java.io.File
|
||||
|
||||
class DirectoryActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var fileAdapter: FileAdapter
|
||||
private lateinit var titleTextView: TextView
|
||||
private lateinit var sumTextView: TextView
|
||||
private var currentDir: FileItem = FileItem("", true, "")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_directory)
|
||||
|
||||
titleTextView = findViewById(R.id.textViewDirPageTitle)
|
||||
sumTextView = findViewById(R.id.textViewDirPageSum)
|
||||
updateTopBar()
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerViewDirectoryList)
|
||||
// recyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
val fileItems = StorageManager.getFilesAndDirectory(this, currentDir.path)
|
||||
fileAdapter = FileAdapter(fileItems) { fileItem ->
|
||||
if (fileItem.isDirectory) {
|
||||
val newFileItems = StorageManager.getFilesAndDirectory(this, fileItem.path)
|
||||
if (newFileItems.size > 0) {
|
||||
fileAdapter.updateFiles(newFileItems)
|
||||
currentDir = fileItem
|
||||
updateTopBar()
|
||||
}
|
||||
} else {
|
||||
// Handle file click
|
||||
Log.d("ithi", "文件点击: ${fileItem.name}")
|
||||
}
|
||||
}
|
||||
recyclerView.adapter = fileAdapter
|
||||
|
||||
val btnBack: ImageButton = findViewById(R.id.btnDirPageBack)
|
||||
btnBack.setOnClickListener {
|
||||
if (currentDir.name == "" || currentDir.name == "files") {
|
||||
finish()
|
||||
} else {
|
||||
val newPath: String = File(currentDir.path).parent ?: ""
|
||||
val newName: String = File(currentDir.path).parentFile?.name ?: ""
|
||||
val newFileItems = StorageManager.getFilesAndDirectory(this, newPath)
|
||||
fileAdapter.updateFiles(newFileItems)
|
||||
currentDir = FileItem(newName, true, newPath)
|
||||
updateTopBar()
|
||||
}
|
||||
}
|
||||
|
||||
val btnSubmit: Button = findViewById(R.id.btnDirPageSubmit)
|
||||
btnSubmit.setOnClickListener {
|
||||
fileAdapter.toggleCheckboxesVisibility()
|
||||
btnSubmit.visibility = View.GONE
|
||||
val selectedFiles = fileAdapter.getSelectedFiles()
|
||||
// 在这里处理选中的文件
|
||||
selectedFiles.forEach { file ->
|
||||
StorageManager.deleteDirectory(this, file.path)
|
||||
}
|
||||
val newFileItems = StorageManager.getFilesAndDirectory(this, currentDir.path)
|
||||
fileAdapter.updateFiles(newFileItems)
|
||||
}
|
||||
|
||||
val btnDelete: ImageButton = findViewById(R.id.btnDirPageDelete)
|
||||
btnDelete.setOnClickListener {
|
||||
if (currentDir.name != "" && currentDir.name != "files") {
|
||||
fileAdapter.toggleCheckboxesVisibility()
|
||||
btnSubmit.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTopBar() {
|
||||
titleTextView.text = if (currentDir.name == "" || currentDir.name == "files") {
|
||||
"内部存储目录"
|
||||
} else {
|
||||
currentDir.name
|
||||
}
|
||||
|
||||
sumTextView.text = StorageManager.getFilesAndDirectorySum(this, currentDir.path).toString()
|
||||
}
|
||||
|
||||
}
|
||||
216
app/src/main/java/com/webtoonviewer/ui/EpisodesActivity.kt
Normal file
@@ -0,0 +1,216 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.db.Book
|
||||
import com.webtoonviewer.network.SmbClientHelper
|
||||
import com.webtoonviewer.repository.BookRepository
|
||||
import com.webtoonviewer.ui.adapter.EpisodeAdapter
|
||||
import com.webtoonviewer.ui.component.DownloadViewModel
|
||||
import com.webtoonviewer.utils.CommonUtils.getEpisodeNameFromEpisode
|
||||
import com.webtoonviewer.utils.CommonUtils.getEllipsizedText
|
||||
import com.webtoonviewer.utils.SharedPreferencesHelper
|
||||
import com.webtoonviewer.utils.Webtoon18Plus.WEBTOON18PLUS
|
||||
import com.webtoonviewer.utils.loadImgFromAssets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class EpisodesActivity : AppCompatActivity() {
|
||||
private val bookRepository: BookRepository = BookRepository(this)
|
||||
private val bookIdPreferences: SharedPreferencesHelper = SharedPreferencesHelper(this, "book_id")
|
||||
private val downloadingPreference: SharedPreferencesHelper = SharedPreferencesHelper(this, "downloading")
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var episodeAdapter: EpisodeAdapter
|
||||
private lateinit var book: Book
|
||||
private lateinit var title: String
|
||||
private lateinit var currentEpisode: String
|
||||
private lateinit var currentEpisodeTextView: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_episodes)
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerViewEpisodesPageEpisodeList)
|
||||
recyclerView.layoutManager = GridLayoutManager(this, 4)
|
||||
|
||||
title = intent.getStringExtra("selectedTitle") ?: ""
|
||||
val thumbnail = intent.getStringExtra("thumbnail") ?: ""
|
||||
if (title != "") {
|
||||
val titleTextView: TextView = findViewById(R.id.textViewEpisodesPageTitle)
|
||||
titleTextView.text = title
|
||||
|
||||
val titleImageView: ImageView = findViewById(R.id.imageViewEpisodesPageImg)
|
||||
val bitmap = loadImgFromAssets(this, thumbnail)
|
||||
if (bitmap != null) {
|
||||
titleImageView.setImageBitmap(bitmap)
|
||||
} else {
|
||||
titleImageView.setImageResource(R.drawable.ic_launcher_background)
|
||||
}
|
||||
|
||||
val btnDownload: ImageButton = findViewById(R.id.btnEpisodesPageDownload)
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val id = bookIdPreferences.readData(title, "0").toInt()
|
||||
book = bookRepository.getBookById(id)!!
|
||||
|
||||
val authorTextView: TextView = findViewById(R.id.textViewEpisodesPageAuthor)
|
||||
authorTextView.text = book.author
|
||||
|
||||
currentEpisodeTextView = findViewById(R.id.textViewEpisodesPageCurrentEpisode)
|
||||
currentEpisode = book.bookmarkChapter
|
||||
setCurrentEpisodeTextView()
|
||||
|
||||
currentEpisodeTextView.setOnClickListener {
|
||||
val intent = Intent(this@EpisodesActivity, ImagesActivity::class.java)
|
||||
intent.putExtra("selectedTitle", book.title)
|
||||
intent.putExtra("selectedChapter", currentEpisode)
|
||||
intent.putExtra("thumbnail", book.img)
|
||||
intent.putExtra("position", book.bookmarkPosition)
|
||||
intent.putExtra("offset", book.bookmarkTopOffset)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val btnDescription: ImageButton = findViewById(R.id.btnEpisodesPageDescription)
|
||||
btnDescription.setOnClickListener {
|
||||
val intent = Intent(this@EpisodesActivity, DescriptionActivity::class.java)
|
||||
intent.putExtra("id", book.id)
|
||||
intent.putExtra("description", book.description)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val isDownloading = downloadingPreference.readData("key", "n")
|
||||
if (book.isLocal == 0 && isDownloading == "n") {
|
||||
btnDownload.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
// 数据加载完毕,更新 UI
|
||||
withContext(Dispatchers.Main) {
|
||||
episodeAdapter = EpisodeAdapter(book.chapters) { selectedChapter ->
|
||||
val intent = Intent(this@EpisodesActivity, ImagesActivity::class.java)
|
||||
|
||||
intent.putExtra("selectedTitle", title)
|
||||
intent.putExtra("selectedChapter", selectedChapter)
|
||||
intent.putExtra("thumbnail", thumbnail)
|
||||
startActivity(intent)
|
||||
}
|
||||
recyclerView.adapter = episodeAdapter
|
||||
}
|
||||
}
|
||||
|
||||
btnDownload.setOnClickListener{
|
||||
btnDownload.visibility = View.GONE
|
||||
downloadingPreference.saveData("key", "y")
|
||||
val smbClientHelper = SmbClientHelper(this)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var isConnect = false
|
||||
try {
|
||||
isConnect = smbClientHelper.connect()
|
||||
if (isConnect) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@EpisodesActivity, "开始下载", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
smbClientHelper.downloadWebtoon(title)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("SMB", "Error accessing SMB share", e)
|
||||
} finally {
|
||||
if (isConnect) {
|
||||
smbClientHelper.disconnect()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@EpisodesActivity, "下载完毕", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
bookRepository.updateIsLocal(book.id, 1)
|
||||
downloadingPreference.saveData("key", "n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val btn18: ImageButton = findViewById(R.id.btnEpisodesPage18)
|
||||
if (WEBTOON18PLUS.contains("$title[18+]")) {
|
||||
btn18.visibility = View.VISIBLE
|
||||
btn18.setOnClickListener {
|
||||
val intent = Intent(this, EpisodesActivity::class.java)
|
||||
intent.putExtra("selectedTitle", "$title[18+]")
|
||||
intent.putExtra("thumbnail", thumbnail)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
val btnBack: ImageButton = findViewById(R.id.btnEpisodesPageHome)
|
||||
btnBack.setOnClickListener {
|
||||
returnToLastActivity()
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
returnToLastActivity()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleScope.launch {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshData() {
|
||||
val id = bookIdPreferences.readData(title, "0").toInt()
|
||||
book = bookRepository.getBookById(id)!!
|
||||
currentEpisode = book.bookmarkChapter
|
||||
setCurrentEpisodeTextView()
|
||||
|
||||
}
|
||||
|
||||
private fun setCurrentEpisodeTextView() {
|
||||
if (currentEpisode != "") {
|
||||
currentEpisodeTextView.post {
|
||||
currentEpisodeTextView.text = getEllipsizedText(
|
||||
currentEpisodeTextView, "${"继续阅读: " + getEpisodeNameFromEpisode(currentEpisode)} >>"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
currentEpisode = book.chapters[0]
|
||||
currentEpisodeTextView.post {
|
||||
currentEpisodeTextView.text = getEllipsizedText(
|
||||
currentEpisodeTextView,
|
||||
"开始阅读: ${getEpisodeNameFromEpisode(currentEpisode)} >>"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToLastActivity() {
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra("bookmarkUpdated", book.title)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
417
app/src/main/java/com/webtoonviewer/ui/ImagesActivity.kt
Normal file
@@ -0,0 +1,417 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.db.Book
|
||||
import com.webtoonviewer.network.SmbClientHelper
|
||||
import com.webtoonviewer.repository.BookRepository
|
||||
import com.webtoonviewer.storage.StorageManager
|
||||
import com.webtoonviewer.ui.adapter.ImageAdapter
|
||||
import com.webtoonviewer.ui.component.SmoothScroller
|
||||
import com.webtoonviewer.utils.CommonUtils.getEpisodeIndexFromEpisode
|
||||
import com.webtoonviewer.utils.CommonUtils.getEpisodeNameFromEpisode
|
||||
import com.webtoonviewer.utils.CommonUtils.getEllipsizedText
|
||||
import com.webtoonviewer.utils.SharedPreferencesHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
|
||||
class ImagesActivity : AppCompatActivity() {
|
||||
|
||||
private val bookRepository: BookRepository = BookRepository(this)
|
||||
private val smbClientHelper: SmbClientHelper = SmbClientHelper(this)
|
||||
private val bookIdPreferences: SharedPreferencesHelper = SharedPreferencesHelper(this, "book_id")
|
||||
private val speedPreference: SharedPreferencesHelper = SharedPreferencesHelper(this, "auto_scroll_speed")
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var imageAdapter: ImageAdapter
|
||||
private lateinit var currentChapter: String
|
||||
private lateinit var title: String
|
||||
private lateinit var book: Book
|
||||
|
||||
private lateinit var topBar: ConstraintLayout
|
||||
private lateinit var bottomBar: ConstraintLayout
|
||||
private var isControlsVisible = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
window.insetsController?.hide(WindowInsets.Type.systemBars())
|
||||
setContentView(R.layout.activity_images)
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerViewImagesList)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
imageAdapter = ImageAdapter(mutableListOf()) {
|
||||
toggleControlsVisibility()
|
||||
}
|
||||
recyclerView.adapter = imageAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
hideBars()
|
||||
|
||||
// Get the height of the RecyclerView and its content
|
||||
val recyclerViewHeight = recyclerView.height
|
||||
val recyclerViewContentHeight = recyclerView.computeVerticalScrollRange()
|
||||
val recyclerViewScrollY = recyclerView.computeVerticalScrollOffset()
|
||||
|
||||
// Check if the RecyclerView has scrolled to the bottom
|
||||
if (recyclerViewContentHeight <= recyclerViewHeight + recyclerViewScrollY) {
|
||||
showBars()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
topBar = findViewById(R.id.imagePageTopBar)
|
||||
bottomBar = findViewById(R.id.imagePageBottomBar)
|
||||
|
||||
currentChapter = intent.getStringExtra("selectedChapter") ?: ""
|
||||
title = intent.getStringExtra("selectedTitle") ?: ""
|
||||
val thumbnail = intent.getStringExtra("thumbnail") ?: ""
|
||||
val position = intent.getIntExtra("position", 0)
|
||||
val offset = intent.getIntExtra("offset", 0)
|
||||
|
||||
val chapterTextView: TextView = findViewById(R.id.textViewImagePageChapter)
|
||||
chapterTextView.post {
|
||||
chapterTextView.text = getEllipsizedText(
|
||||
chapterTextView, getEpisodeNameFromEpisode(currentChapter)
|
||||
)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
book = getBook()
|
||||
if (position == 0 && offset == 0) {
|
||||
loadImagesIncrementally()
|
||||
} else {
|
||||
loadImagesAllAtOnce(position, offset)
|
||||
}
|
||||
}
|
||||
|
||||
val btnPrev: ImageButton = findViewById(R.id.btnImagePagePrev)
|
||||
btnPrev.setOnClickListener {
|
||||
if (hasPrevChapter(currentChapter)) {
|
||||
val newChapter = getPrevChapter(currentChapter)
|
||||
currentChapter = newChapter
|
||||
imageAdapter.initImages()
|
||||
loadImagesAllAtOnce(-1, -1)
|
||||
chapterTextView.post {
|
||||
chapterTextView.text = getEllipsizedText(
|
||||
chapterTextView, getEpisodeNameFromEpisode(currentChapter)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this@ImagesActivity, "这是最初的章节。", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
val btnNext: ImageButton = findViewById(R.id.btnImagePageNext)
|
||||
btnNext.setOnClickListener {
|
||||
Log.d("ithi", "currentChapter: $currentChapter")
|
||||
|
||||
if (hasNextChapter(currentChapter)) {
|
||||
val newChapter = getNextChapter(currentChapter)
|
||||
currentChapter = newChapter
|
||||
imageAdapter.initImages()
|
||||
loadImagesIncrementally()
|
||||
scrollToTop()
|
||||
chapterTextView.post {
|
||||
chapterTextView.text = getEllipsizedText(
|
||||
chapterTextView, getEpisodeNameFromEpisode(currentChapter)
|
||||
)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
savePosition()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this@ImagesActivity, "这已经是最后一话了。", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
val btnBack: ImageButton = findViewById(R.id.btnImagePageBack)
|
||||
btnBack.setOnClickListener {
|
||||
returnToLastActivity()
|
||||
}
|
||||
|
||||
val btnHome: ImageButton = findViewById(R.id.btnImagePageHome)
|
||||
btnHome.setOnClickListener {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.putExtra("bookmarkUpdated", book.title)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val btnList: ImageButton = findViewById(R.id.btnImagePageList)
|
||||
btnList.setOnClickListener {
|
||||
val intent = Intent(this@ImagesActivity, EpisodesActivity::class.java)
|
||||
intent.putExtra("selectedTitle", title)
|
||||
intent.putExtra("thumbnail", thumbnail)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val btnPlay: ImageButton = findViewById(R.id.btnImagePagePlay)
|
||||
btnPlay.setOnClickListener {
|
||||
val bottomOffset = calculateBottomOffset()
|
||||
val speed = speedPreference.readData(title, "1x")
|
||||
smoothScrollToBottomWithOffset(bottomOffset, speed)
|
||||
}
|
||||
|
||||
val btnMenu: ImageButton = findViewById(R.id.btnImagePageMenu)
|
||||
|
||||
btnMenu.setOnClickListener {
|
||||
val dialog = DialogAutoScrollSpeedFragment.newInstance(title)
|
||||
dialog.isCancelable = true
|
||||
dialog.show(supportFragmentManager, "DialogAutoScrollSpeedFragment")
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
returnToLastActivity()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadImagesIncrementally() {
|
||||
if (book.isLocal == 1) {
|
||||
loadImagesAllAtOnce(0, 0)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
var isConnect = false
|
||||
try {
|
||||
isConnect = smbClientHelper.connect()
|
||||
if (isConnect) {
|
||||
val newImagePaths = getImagePathsFromShare()
|
||||
newImagePaths.forEach {
|
||||
val bitmap = loadImageFromShare(it)
|
||||
if (bitmap != null) {
|
||||
imageAdapter.addImage(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (isConnect) {
|
||||
smbClientHelper.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImagesAllAtOnce(position: Int, offset: Int) {
|
||||
if (book.isLocal == 1) {
|
||||
lifecycleScope.launch {
|
||||
val newImagePaths = getImagePathsFromStorage(book.title, currentChapter)
|
||||
val bitmaps = withContext(Dispatchers.IO) {
|
||||
newImagePaths.mapNotNull { loadImageFromStorage(it) }
|
||||
}
|
||||
imageAdapter.updateImages(bitmaps)
|
||||
if (position == -1 && offset == -1) {
|
||||
val newPosition = bitmaps.size - 1
|
||||
val newOffset = calculateBottomOffset()
|
||||
scrollToPosition(newPosition, newOffset)
|
||||
} else {
|
||||
scrollToPosition(position, offset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
var isConnect = false
|
||||
try {
|
||||
isConnect = smbClientHelper.connect()
|
||||
if (isConnect) {
|
||||
val newImagePaths = getImagePathsFromShare()
|
||||
val bitmaps = withContext(Dispatchers.IO) {
|
||||
newImagePaths.mapNotNull { loadImageFromShare(it) }
|
||||
}
|
||||
imageAdapter.updateImages(bitmaps)
|
||||
if (position == -1 && offset == -1) {
|
||||
val newPosition = bitmaps.size - 1
|
||||
val newOffset = calculateBottomOffset()
|
||||
scrollToPosition(newPosition, newOffset)
|
||||
} else {
|
||||
scrollToPosition(position, offset)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (isConnect) {
|
||||
smbClientHelper.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getImagePathsFromShare(): MutableList<String> {
|
||||
return smbClientHelper.getImagePathsFromShare(title, currentChapter)
|
||||
}
|
||||
|
||||
private suspend fun loadImageFromShare(imagePath: String): Bitmap? {
|
||||
return smbClientHelper.loadImageFromShare(imagePath)
|
||||
}
|
||||
|
||||
private fun getImagePathsFromStorage(title: String, chapter: String): MutableList<File> {
|
||||
return StorageManager.getImagePaths(this, title, chapter)
|
||||
}
|
||||
|
||||
private fun loadImageFromStorage(imagePath: File): Bitmap? {
|
||||
return StorageManager.loadImage(imagePath)
|
||||
}
|
||||
|
||||
private suspend fun getBook(): Book {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bookId = bookIdPreferences.readData(title, "0").toInt()
|
||||
bookRepository.getBookById(bookId)!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasPrevChapter(selectedChapter: String): Boolean {
|
||||
val chapterIndex = getEpisodeIndexFromEpisode(selectedChapter)
|
||||
return chapterIndex > 0
|
||||
}
|
||||
|
||||
private fun hasNextChapter(selectedChapter: String): Boolean {
|
||||
val chapters = book.chapters
|
||||
val chapterIndex = getEpisodeIndexFromEpisode(selectedChapter)
|
||||
return chapterIndex < chapters.size - 1
|
||||
}
|
||||
|
||||
private fun getPrevChapter(selectedChapter: String): String {
|
||||
val chapters = book.chapters
|
||||
val chapterIndex = getEpisodeIndexFromEpisode(selectedChapter)
|
||||
return chapters[chapterIndex - 1]
|
||||
}
|
||||
|
||||
private fun getNextChapter(selectedChapter: String): String {
|
||||
val chapters = book.chapters
|
||||
val chapterIndex = getEpisodeIndexFromEpisode(selectedChapter)
|
||||
return chapters[chapterIndex + 1]
|
||||
}
|
||||
|
||||
private fun generateChapterName(): String {
|
||||
val chapter = getEpisodeNameFromEpisode(currentChapter)
|
||||
if (chapter.contains(" ")) {
|
||||
val parts = chapter.split(" ")
|
||||
if (parts.size == 2) {
|
||||
return parts[0]
|
||||
} else if (parts.size >= 3) {
|
||||
return parts[0] + parts[parts.size - 1]
|
||||
}
|
||||
} else if (chapter.length > 5) {
|
||||
return chapter.substring(0, 4) + "..."
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
|
||||
private suspend fun savePosition() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
|
||||
layoutManager?.let {
|
||||
val scrollPosition = it.findFirstVisibleItemPosition()
|
||||
val topView = recyclerView.getChildAt(0)
|
||||
val topOffset = topView?.top ?: 0
|
||||
bookRepository.updateBookmarkById(book.id, currentChapter, scrollPosition,topOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
(recyclerView.layoutManager as LinearLayoutManager).scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun scrollToPosition(position: Int, offset: Int) {
|
||||
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, offset)
|
||||
}
|
||||
|
||||
private fun toggleControlsVisibility() {
|
||||
if (isControlsVisible) {
|
||||
hideBars()
|
||||
} else {
|
||||
showBars()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideBars() {
|
||||
topBar.visibility = View.GONE
|
||||
bottomBar.visibility = View.GONE
|
||||
isControlsVisible = false
|
||||
window.insetsController?.hide(WindowInsets.Type.systemBars())
|
||||
}
|
||||
|
||||
private fun showBars() {
|
||||
topBar.visibility = View.VISIBLE
|
||||
bottomBar.visibility = View.VISIBLE
|
||||
isControlsVisible = true
|
||||
}
|
||||
|
||||
private fun calculateBottomOffset(): Int {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||
val lastVisibleItemView = layoutManager.findViewByPosition(lastVisibleItemPosition)
|
||||
|
||||
val recyclerViewHeight = recyclerView.height
|
||||
val bottomOffset = if (lastVisibleItemView != null) {
|
||||
recyclerViewHeight - (lastVisibleItemView.bottom - lastVisibleItemView.top)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
return bottomOffset
|
||||
}
|
||||
|
||||
private fun smoothScrollToBottomWithOffset(offset: Int, speed: String) {
|
||||
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
||||
val smoothScroller = SmoothScroller(this, offset, speed)
|
||||
|
||||
// 设置滚动到最后一项并添加底部偏移量
|
||||
smoothScroller.targetPosition = imageAdapter.itemCount - 1
|
||||
layoutManager.startSmoothScroll(smoothScroller)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
lifecycleScope.launch {
|
||||
savePosition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
lifecycleScope.launch {
|
||||
savePosition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleScope.launch {
|
||||
savePosition()
|
||||
smbClientHelper.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToLastActivity() {
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra("bookmarkUpdated", book.title)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
167
app/src/main/java/com/webtoonviewer/ui/MainActivity.kt
Normal file
@@ -0,0 +1,167 @@
|
||||
package com.webtoonviewer.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.repository.BookRepository
|
||||
import com.webtoonviewer.ui.adapter.TitleAdapter
|
||||
import com.webtoonviewer.ui.data.TitleItem
|
||||
import com.webtoonviewer.utils.SharedPreferencesHelper
|
||||
import com.webtoonviewer.utils.Webtoon18Plus.WEBTOON18PLUS
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity : AppCompatActivity(), TitleAdapter.OnItemClickListener {
|
||||
|
||||
private val bookIdPreferences: SharedPreferencesHelper = SharedPreferencesHelper(this, "book_id")
|
||||
private val tagsVisibilityPreferences: SharedPreferencesHelper = SharedPreferencesHelper(this, "tags_visibility")
|
||||
private lateinit var bookRepository: BookRepository
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var mainAdapter: TitleAdapter
|
||||
private lateinit var titleItems: MutableList<TitleItem>
|
||||
|
||||
// private val manageExternalStoragePermissionLauncher =
|
||||
// registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
// if (android.os.Environment.isExternalStorageManager()) {
|
||||
// Toast.makeText(this, "外部存储权限已授予", Toast.LENGTH_SHORT).show()
|
||||
// } else {
|
||||
// Toast.makeText(this, "外部存储权限被拒绝", Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// }
|
||||
|
||||
private val startForResult =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val data: Intent? = result.data
|
||||
val title = data?.getStringExtra("bookmarkUpdated")
|
||||
title?.let {
|
||||
// 在当前 Activity 的协程作用域中调用 refreshData
|
||||
lifecycleScope.launch {
|
||||
refreshData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val textViewWV : TextView = findViewById(R.id.textViewMainPage)
|
||||
textViewWV.setOnClickListener {
|
||||
refreshActivity()
|
||||
}
|
||||
|
||||
val settingsButton: ImageButton = findViewById(R.id.btnMainPage)
|
||||
settingsButton.setOnClickListener {
|
||||
val dialog = DialogMainSettingsFragment()
|
||||
dialog.show(supportFragmentManager, "DialogMainSettingsFragment")
|
||||
}
|
||||
|
||||
bookRepository = BookRepository(this)
|
||||
|
||||
// if (!externalStoragePermission.hasManageExternalStoragePermission()) {
|
||||
// externalStoragePermission.requestManageExternalStoragePermission(
|
||||
// manageExternalStoragePermissionLauncher
|
||||
// )
|
||||
// }
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerViewTitleList)
|
||||
recyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||
|
||||
titleItems = mutableListOf()
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val titles = bookRepository.getAllBookTitles()
|
||||
titleItems = generateTitleItems(titles)
|
||||
}
|
||||
// 数据加载完毕,更新 UI
|
||||
withContext(Dispatchers.Main) {
|
||||
mainAdapter = TitleAdapter(this@MainActivity, titleItems, this@MainActivity)
|
||||
recyclerView.adapter = mainAdapter
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
val bookmarkUpdated = intent.getStringExtra("bookmarkUpdated") ?: ""
|
||||
if (bookmarkUpdated != "") {
|
||||
refreshData(bookmarkUpdated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun generateTitleItems(titles: MutableList<String>): MutableList<TitleItem> {
|
||||
val titleItems: MutableList<TitleItem> = mutableListOf()
|
||||
withContext(Dispatchers.IO) {
|
||||
for (title in titles) {
|
||||
if (!WEBTOON18PLUS.contains(title)) {
|
||||
val id = bookIdPreferences.readData(title, "0").toInt()
|
||||
val book = bookRepository.getBookById(id)
|
||||
val tag = book?.tag ?: ""
|
||||
if (tagsVisibilityPreferences.getBoolean("showAll", false) ||
|
||||
tagsVisibilityPreferences.getBoolean(tag, false)) {
|
||||
val imageName = book?.img ?: ""
|
||||
val chapter = if (book?.bookmarkChapter.isNullOrEmpty()) {
|
||||
book?.chapters?.get(0).toString()
|
||||
} else {
|
||||
book?.bookmarkChapter ?: ""
|
||||
}
|
||||
val position = book?.bookmarkPosition ?: 0
|
||||
val offset = book?.bookmarkTopOffset ?: 0
|
||||
titleItems.add(TitleItem(imageName, title, tag, chapter, position, offset))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return titleItems
|
||||
}
|
||||
|
||||
override fun clickToEpisodeList(titleItem: TitleItem) {
|
||||
val intent = Intent(this, EpisodesActivity::class.java)
|
||||
intent.putExtra("selectedTitle", titleItem.title)
|
||||
intent.putExtra("thumbnail", titleItem.thumbnail)
|
||||
startForResult.launch(intent)
|
||||
}
|
||||
|
||||
override fun clickToSingleEpisode(titleItem: TitleItem) {
|
||||
val intent = Intent(this, ImagesActivity::class.java)
|
||||
intent.putExtra("selectedTitle", titleItem.title)
|
||||
intent.putExtra("selectedChapter", titleItem.chapter)
|
||||
intent.putExtra("thumbnail", titleItem.thumbnail)
|
||||
intent.putExtra("position", titleItem.position)
|
||||
intent.putExtra("offset", titleItem.offset)
|
||||
startForResult.launch(intent)
|
||||
}
|
||||
|
||||
private suspend fun refreshData(title: String) {
|
||||
val titleItem = titleItems.find { it.title == title }
|
||||
val id = bookIdPreferences.readData(title, "0").toInt()
|
||||
val book = bookRepository.getBookById(id)
|
||||
|
||||
titleItem?.chapter = if (book?.bookmarkChapter.isNullOrEmpty()) {
|
||||
book?.chapters?.get(0).toString()
|
||||
} else {
|
||||
book?.bookmarkChapter ?: ""
|
||||
}
|
||||
titleItem?.position = book?.bookmarkPosition ?: 0
|
||||
titleItem?.offset = book?.bookmarkTopOffset ?: 0
|
||||
if (titleItem != null) {
|
||||
mainAdapter.updateItem(titleItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshActivity() {
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.utils.CommonUtils.getEllipsizedText
|
||||
import com.webtoonviewer.utils.CommonUtils.getShortNameFromEpisode
|
||||
|
||||
class EpisodeAdapter(
|
||||
private val chapters: MutableList<String>,
|
||||
private val onItemClick: (String) -> Unit
|
||||
) : RecyclerView.Adapter<EpisodeAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val chapterTextView: TextView = itemView.findViewById(R.id.tvItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_text, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val chapterTextView: TextView = holder.chapterTextView
|
||||
chapterTextView.post {
|
||||
// chapterTextView.text = getEllipsizedText(chapterTextView, chapters[position].split(".")[1])
|
||||
chapterTextView.text = getEllipsizedText(chapterTextView, getShortNameFromEpisode(chapters[position]))
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(chapters[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return chapters.size
|
||||
}
|
||||
|
||||
}
|
||||
115
app/src/main/java/com/webtoonviewer/ui/adapter/FileAdapter.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.text.format.Formatter.formatFileSize
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.ui.data.FileItem
|
||||
import com.webtoonviewer.utils.CommonUtils.getEllipsizedText
|
||||
import com.webtoonviewer.utils.CommonUtils.getShortNameFromEpisode
|
||||
import java.io.File
|
||||
|
||||
class FileAdapter(
|
||||
private val fileList: MutableList<FileItem>,
|
||||
private val onItemClick: (FileItem) -> Unit
|
||||
) : RecyclerView.Adapter<FileAdapter.ViewHolder>() {
|
||||
|
||||
private var showCheckboxes: Boolean = false
|
||||
private val selectedFiles = mutableListOf<FileItem>()
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val fileNameTextView: TextView = itemView.findViewById(R.id.textViewFileNameItem)
|
||||
val infoTextView: TextView = itemView.findViewById(R.id.textViewFileInfoItem)
|
||||
val iconImageView: ImageView = itemView.findViewById(R.id.file_icon)
|
||||
val checkBox: CheckBox = itemView.findViewById(R.id.file_checkbox)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_file, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val file = fileList[position]
|
||||
val fileNameTextView: TextView = holder.fileNameTextView
|
||||
fileNameTextView.text = file.name
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(file)
|
||||
}
|
||||
holder.checkBox.visibility = if (showCheckboxes) View.VISIBLE else View.GONE
|
||||
holder.checkBox.isChecked = selectedFiles.contains(file)
|
||||
|
||||
holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
// 如果选中,加入选中的文件列表
|
||||
selectedFiles.add(file)
|
||||
} else {
|
||||
// 如果取消选中,移除选中的文件
|
||||
selectedFiles.remove(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (File(file.path).isDirectory) {
|
||||
// 如果是目录,显示目录图标和包含项目数量
|
||||
holder.iconImageView.setImageResource(R.drawable.folder_18) // 设置为目录图标
|
||||
val itemsCount = File(file.path).listFiles()?.size ?: 0
|
||||
holder.infoTextView.text = "$itemsCount items"
|
||||
} else {
|
||||
// 如果是文件,显示文件图标和文件大小
|
||||
holder.iconImageView.setImageResource(R.drawable.document_18) // 设置为文件图标
|
||||
val fileSize = formatFileSize(File(file.path).length()) // 格式化文件大小
|
||||
holder.infoTextView.text = fileSize
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return fileList.size
|
||||
}
|
||||
|
||||
private fun initFiles() {
|
||||
fileList.clear()
|
||||
}
|
||||
|
||||
private fun sortFiles(fileItems: MutableList<FileItem>): MutableList<FileItem> {
|
||||
val sortedList: MutableList<FileItem> = fileItems
|
||||
if (sortedList[0].name.split(".").isNotEmpty())
|
||||
sortedList.sortBy {
|
||||
it.name.substringBefore('.').toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
return sortedList
|
||||
}
|
||||
|
||||
fun updateFiles(fileItems: MutableList<FileItem>) {
|
||||
initFiles()
|
||||
val sortedFiles = sortFiles(fileItems)
|
||||
fileList.addAll(sortedFiles)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun formatFileSize(size: Long): String {
|
||||
val kb = size / 1024.0
|
||||
val mb = kb / 1024.0
|
||||
return when {
|
||||
mb >= 1 -> String.format("%.2f MB", mb)
|
||||
kb >= 1 -> String.format("%.2f KB", kb)
|
||||
else -> "$size B"
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCheckboxesVisibility() {
|
||||
showCheckboxes = !showCheckboxes
|
||||
notifyDataSetChanged() // 刷新 RecyclerView
|
||||
}
|
||||
|
||||
fun getSelectedFiles(): List<FileItem> {
|
||||
return selectedFiles
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
|
||||
class ImageAdapter(
|
||||
private var images: MutableList<Bitmap>,
|
||||
private val onClick: () -> Unit,
|
||||
) : RecyclerView.Adapter<ImageAdapter.ImageViewHolder>() {
|
||||
|
||||
class ImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView = view.findViewById(R.id.imageViewImageItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
|
||||
return ImageViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
holder.imageView.setImageBitmap(images[position])
|
||||
holder.itemView.setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = images.size
|
||||
|
||||
fun initImages() {
|
||||
images.clear()
|
||||
}
|
||||
|
||||
fun addImage(bitmap: Bitmap) {
|
||||
images.add(bitmap)
|
||||
notifyItemInserted(images.size - 1)
|
||||
}
|
||||
|
||||
fun updateImages(bitmaps: List<Bitmap>) {
|
||||
initImages()
|
||||
images.addAll(bitmaps)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
46
app/src/main/java/com/webtoonviewer/ui/adapter/TagAdapter.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.ui.data.CheckboxItem
|
||||
import kotlin.math.log
|
||||
|
||||
class TagAdapter(
|
||||
private val items: List<CheckboxItem>,
|
||||
private val onCheckedChangeListener: (position: Int, isChecked: Boolean) -> Unit
|
||||
) : RecyclerView.Adapter<TagAdapter.MenuViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MenuViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_checkbox, parent, false)
|
||||
return MenuViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: MenuViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
Log.d("ithi", "onBindViewHolder: $item")
|
||||
holder.checkBox.isChecked = item.isChecked
|
||||
holder.checkBox.text = item.text
|
||||
|
||||
holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
|
||||
onCheckedChangeListener(position, isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
class MenuViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val checkBox: CheckBox = itemView.findViewById(R.id.checkBoxDialogMainSettings)
|
||||
}
|
||||
|
||||
fun setItemsChecked() {
|
||||
items.forEach {
|
||||
it.isChecked = true
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
import com.webtoonviewer.ui.data.TitleItem
|
||||
import com.webtoonviewer.utils.CommonUtils.getEllipsizedText
|
||||
import com.webtoonviewer.utils.CommonUtils.getShortNameFromEpisode
|
||||
import com.webtoonviewer.utils.loadImgFromAssets
|
||||
|
||||
class TitleAdapter(
|
||||
private val context: Context,
|
||||
private val titleItems: MutableList<TitleItem>,
|
||||
private val onItemClick: OnItemClickListener
|
||||
) : RecyclerView.Adapter<TitleItemViewHolder>() {
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun clickToEpisodeList(titleItem: TitleItem)
|
||||
fun clickToSingleEpisode(titleItem: TitleItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleItemViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_title,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return TitleItemViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TitleItemViewHolder, position: Int) {
|
||||
val titleItem = titleItems[position]
|
||||
|
||||
val bitmap = loadImgFromAssets(context, titleItem.thumbnail)
|
||||
if (bitmap != null) {
|
||||
holder.thumbnail.setImageBitmap(bitmap)
|
||||
} else {
|
||||
holder.thumbnail.setImageResource(R.drawable.ic_launcher_background)
|
||||
}
|
||||
|
||||
holder.title.text = titleItem.title
|
||||
holder.tag.text = titleItem.tag
|
||||
val chapterTextView = holder.chapter
|
||||
if (titleItem.chapter != "") {
|
||||
chapterTextView.post{
|
||||
chapterTextView.text = getEllipsizedText(chapterTextView, "${getShortNameFromEpisode(titleItem.chapter)} >>")
|
||||
}
|
||||
}
|
||||
|
||||
holder.thumbnail.setOnClickListener {
|
||||
onItemClick.clickToEpisodeList(titleItem)
|
||||
}
|
||||
|
||||
holder.title.setOnClickListener {
|
||||
onItemClick.clickToEpisodeList(titleItem)
|
||||
}
|
||||
|
||||
holder.tag.setOnClickListener {
|
||||
onItemClick.clickToEpisodeList(titleItem)
|
||||
}
|
||||
|
||||
holder.chapter.setOnClickListener {
|
||||
onItemClick.clickToSingleEpisode(titleItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return titleItems.size
|
||||
}
|
||||
|
||||
fun updateItem(titleItem: TitleItem) {
|
||||
val index = titleItems.indexOf(titleItem)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.webtoonviewer.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.webtoonviewer.R
|
||||
|
||||
class TitleItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val thumbnail: ImageView = itemView.findViewById(R.id.imageViewTitlePageThumbnail)
|
||||
val title: TextView = itemView.findViewById(R.id.textViewTitlePageTitle)
|
||||
val tag: TextView = itemView.findViewById(R.id.textViewTitlePageTag)
|
||||
val chapter: TextView = itemView.findViewById(R.id.textViewTitlePageChapter)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.webtoonviewer.ui.component
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class DownloadViewModel : ViewModel() {
|
||||
private val _isDownloading = MutableLiveData<Boolean>(false)
|
||||
val isDownloading: LiveData<Boolean> get() = _isDownloading
|
||||
|
||||
fun startDownload() {
|
||||
_isDownloading.value = true
|
||||
}
|
||||
|
||||
fun finishDownload() {
|
||||
_isDownloading.value = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.webtoonviewer.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
|
||||
class SmoothScroller(
|
||||
context: Context,
|
||||
private val offset: Int,
|
||||
private val speedFactor: String
|
||||
) : LinearSmoothScroller(context) {
|
||||
|
||||
override fun calculateDxToMakeVisible(view: View, snapPreference: Int): Int {
|
||||
val dx = super.calculateDxToMakeVisible(view, snapPreference)
|
||||
return dx + offset // 添加偏移量
|
||||
}
|
||||
|
||||
override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
|
||||
val dy = super.calculateDyToMakeVisible(view, snapPreference)
|
||||
return dy + offset // 添加偏移量
|
||||
}
|
||||
|
||||
// 控制滚动速度
|
||||
override fun calculateSpeedPerPixel(displayMetrics: android.util.DisplayMetrics): Float {
|
||||
return generateSpeed(speedFactor) / displayMetrics.densityDpi
|
||||
}
|
||||
|
||||
override fun getVerticalSnapPreference(): Int {
|
||||
return SNAP_TO_START
|
||||
}
|
||||
|
||||
private fun generateSpeed(speedFactor: String):Float {
|
||||
return when (speedFactor) {
|
||||
"1x" -> {
|
||||
2000f
|
||||
}
|
||||
"2x" -> {
|
||||
1600f
|
||||
}
|
||||
"3x" -> {
|
||||
1200f
|
||||
}
|
||||
"4x" -> {
|
||||
800f
|
||||
}
|
||||
"5x" -> {
|
||||
400f
|
||||
}
|
||||
else -> {
|
||||
2000f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.webtoonviewer.ui.data
|
||||
|
||||
data class CheckboxItem(
|
||||
val text: String,
|
||||
var isChecked: Boolean
|
||||
)
|
||||
7
app/src/main/java/com/webtoonviewer/ui/data/FileItem.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.webtoonviewer.ui.data
|
||||
|
||||
data class FileItem(
|
||||
val name: String,
|
||||
val isDirectory: Boolean,
|
||||
val path: String
|
||||
)
|
||||
10
app/src/main/java/com/webtoonviewer/ui/data/TitleItem.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.webtoonviewer.ui.data
|
||||
|
||||
data class TitleItem(
|
||||
val thumbnail: String,
|
||||
val title: String,
|
||||
val tag: String,
|
||||
var chapter: String,
|
||||
var position: Int,
|
||||
var offset: Int
|
||||
)
|
||||
78
app/src/main/java/com/webtoonviewer/utils/CommonUtils.kt
Normal file
@@ -0,0 +1,78 @@
|
||||
package com.webtoonviewer.utils
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.widget.TextView
|
||||
import java.nio.file.Paths
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object CommonUtils {
|
||||
fun getEpisodeNameFromEpisode(episode: String): String {
|
||||
val (_, name) = splitEpisode(episode)
|
||||
return name
|
||||
}
|
||||
|
||||
fun getShortNameFromEpisode(episode: String): String {
|
||||
val pattern = "第(\\d+)話"
|
||||
val regex = Regex(pattern)
|
||||
var name = getEpisodeNameFromEpisode(episode)
|
||||
|
||||
if (regex.matches(name.split(" ")[0])) {
|
||||
name = name.split(" ")[0]
|
||||
} else if (name.split(" ").size > 1 && regex.matches(name.split(" ")[1])) {
|
||||
name = name.split(" ")[1]
|
||||
} else if (name.split(" ")[0].contains("Ep")) {
|
||||
name = name.split(" ")[0] + name.split(" ")[name.split(" ").size - 1]
|
||||
} else if (name.split(" ").size > 1) {
|
||||
name = name.split(" ")[0]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
fun getEpisodeIndexFromEpisode(episode: String): Int {
|
||||
val (index, _) = splitEpisode(episode)
|
||||
return index
|
||||
}
|
||||
|
||||
private fun splitEpisode(episode: String): Pair<Int, String> {
|
||||
val matcher = Pattern.compile("(\\d+)\\.(.+)").matcher(episode)
|
||||
var index = 0
|
||||
var name = ""
|
||||
if (matcher.matches()) {
|
||||
index = matcher.group(1)?.toInt() ?: 0
|
||||
name = matcher.group(2)?.toString() ?: ""
|
||||
}
|
||||
return Pair(index, name)
|
||||
}
|
||||
|
||||
fun pathJoin(vararg parts: String): String {
|
||||
return Paths.get(parts[0], *parts.sliceArray(1 until parts.size)).toString()
|
||||
}
|
||||
|
||||
fun getEllipsizedText(textView: TextView, originalText: String): String {
|
||||
val availableWidth = textView.width - textView.paddingLeft - textView.paddingRight
|
||||
val textPaint = textView.paint
|
||||
|
||||
if (availableWidth > textPaint.measureText(originalText)) {
|
||||
return originalText
|
||||
} else {
|
||||
val parts = originalText.split(" ")
|
||||
var maxLength = 0
|
||||
var maxIndex = -1
|
||||
for ((index, part) in parts.withIndex()) {
|
||||
if (part.length > maxLength) {
|
||||
maxIndex = index
|
||||
maxLength = part.length
|
||||
}
|
||||
}
|
||||
val restParts = parts.toMutableList()
|
||||
restParts.removeAt(maxIndex)
|
||||
|
||||
val ellipsizedText = TextUtils.ellipsize(
|
||||
parts[maxIndex], textPaint,
|
||||
(availableWidth - textPaint.measureText(restParts.joinToString(" "))),
|
||||
TextUtils.TruncateAt.END).toString()
|
||||
|
||||
return originalText.replace(parts[maxIndex], ellipsizedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.webtoonviewer.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
class ExternalStorageHelper {
|
||||
|
||||
private val baseDir = File(Environment.getExternalStorageDirectory(), "Webtoon")
|
||||
private val baseTempDir = File(Environment.getExternalStorageDirectory(), "Temp_Webtoon")
|
||||
|
||||
fun hasTempWebtoon(): Boolean {
|
||||
return baseTempDir.exists()
|
||||
}
|
||||
|
||||
fun getAllWebtoonTitles(): MutableList<String> {
|
||||
return getAllWebtoonTitles(baseDir)
|
||||
}
|
||||
|
||||
fun getAllTempWebtoonTitles(): MutableList<String> {
|
||||
return getAllWebtoonTitles(baseTempDir)
|
||||
}
|
||||
|
||||
private fun getAllWebtoonTitles(dir: File): MutableList<String> {
|
||||
val titles = mutableListOf<String>()
|
||||
val items = dir.listFiles()
|
||||
if (items != null) {
|
||||
for (item in items) {
|
||||
if (item.isDirectory) {
|
||||
titles.add(item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
fun getAllEpisodes(title: String): MutableList<String> {
|
||||
val webtoonDir = File(baseDir, title)
|
||||
val episodes = mutableListOf<String>()
|
||||
val items = webtoonDir.listFiles()
|
||||
if (items != null) {
|
||||
for (item in items) {
|
||||
if (item.isDirectory) {
|
||||
episodes.add(item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
|
||||
fun getImagePaths(title: String, episode: String): MutableList<File> {
|
||||
val episodeDir = File(baseDir, "$title/$episode")
|
||||
val imagePaths = mutableListOf<File>()
|
||||
val items = episodeDir.listFiles()
|
||||
val sum = items?.size ?: 0
|
||||
for (i in 1 until sum + 1) {
|
||||
imagePaths.add(File(episodeDir,"$i.jpg"))
|
||||
}
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
fun loadImage(imagePath: File): Bitmap? {
|
||||
return if (imagePath.exists()) {
|
||||
val inputStream = FileInputStream(imagePath)
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
} else null
|
||||
}
|
||||
|
||||
fun createEpisode(title: String, episode: String) {
|
||||
val file = File(baseDir, "$title/$episode")
|
||||
if (!file.exists()) {
|
||||
file.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun episodeExists(title: String, episode: String): Boolean {
|
||||
return File(baseDir, "$title/$episode").exists()
|
||||
}
|
||||
|
||||
fun generateOutputFile(imagePath: String): File? {
|
||||
val parts = imagePath.split("/")
|
||||
return if (parts.size > 1) {
|
||||
val result = parts.drop(1).joinToString("/")
|
||||
return File(baseDir, result)
|
||||
} else null
|
||||
|
||||
}
|
||||
}
|
||||