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