Initial commit: Add webtoon viewer

This commit is contained in:
2024-12-19 14:09:30 +01:00
commit 3a566a08fa
642 changed files with 4769 additions and 0 deletions

15
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/compiler.xml generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/build

58
app/build.gradle.kts Normal file
View 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
View 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

View File

@@ -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)
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
app/src/main/assets/116.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
app/src/main/assets/142.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
app/src/main/assets/152.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
app/src/main/assets/167.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
app/src/main/assets/200.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
app/src/main/assets/222.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
app/src/main/assets/230.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
app/src/main/assets/231.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
app/src/main/assets/233.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
app/src/main/assets/247.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
app/src/main/assets/255.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
app/src/main/assets/303.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
app/src/main/assets/322.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
app/src/main/assets/330.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
app/src/main/assets/368.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
app/src/main/assets/399.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
app/src/main/assets/41.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
app/src/main/assets/421.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
app/src/main/assets/424.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
app/src/main/assets/429.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
app/src/main/assets/433.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
app/src/main/assets/450.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
app/src/main/assets/484.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
app/src/main/assets/489.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
app/src/main/assets/49.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
app/src/main/assets/516.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
app/src/main/assets/53.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
app/src/main/assets/545.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
app/src/main/assets/547.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
app/src/main/assets/587.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
app/src/main/assets/591.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
app/src/main/assets/595.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
app/src/main/assets/617.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
app/src/main/assets/711.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
app/src/main/assets/736.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
app/src/main/assets/784.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
app/src/main/assets/787.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
app/src/main/assets/810.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
app/src/main/assets/814.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
app/src/main/assets/862.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View 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)
}
}
}

View 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")
}
}
}
}

View 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)
}
}

View 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()
}

View File

@@ -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"
}

View 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)
}
}
}

View File

@@ -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
)

View File

@@ -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)
}
}

View 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
}
}

View 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)
}
}

View File

@@ -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?) {}
})
}
}

View File

@@ -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
}
}

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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)
}
}

View File

@@ -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
}
}

View 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
}
}

View File

@@ -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()
}
}

View 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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,6 @@
package com.webtoonviewer.ui.data
data class CheckboxItem(
val text: String,
var isChecked: Boolean
)

View File

@@ -0,0 +1,7 @@
package com.webtoonviewer.ui.data
data class FileItem(
val name: String,
val isDirectory: Boolean,
val path: String
)

View 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
)

View 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)
}
}
}

View File

@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More