Add cxyz Android project structure and initial configuration

- Introduced a new Flutter project for the cxyz Android manga/comic reader, including essential files such as pubspec.yaml, AndroidManifest.xml, and Gradle configuration.
- Added .gitignore files to exclude unnecessary files and directories for both the main project and Android-specific files.
- Implemented initial README.md with project description, features, setup instructions, and API details for ComicLand, MangaRead, and MangaTown.
- Configured analysis options and dependencies in pubspec.yaml to support Flutter development.
- Established a pnpm workspace and lockfile for dependency management.
- Included various Android resources such as icons, styles, and launch backgrounds for a complete application setup.
This commit is contained in:
2026-08-29 17:50:31 +07:00
parent 6e7f1ced35
commit a7c6058540
105 changed files with 16721 additions and 275 deletions
@@ -0,0 +1,52 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<application
android:label="cxyz"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="false"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,153 @@
package org.cxyz.cxyz_android
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
class MainActivity : FlutterActivity() {
private val channelName = "org.cxyz.cxyz_android/cbz_store"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"saveCbzFromPath" -> {
val relativeDir = call.argument<String>("relativeDir")
val displayName = call.argument<String>("displayName")
val sourcePath = call.argument<String>("sourcePath")
if (relativeDir == null || displayName == null || sourcePath == null) {
result.error(
"bad_args",
"relativeDir, displayName, sourcePath required",
null,
)
return@setMethodCallHandler
}
try {
result.success(saveCbzFromPath(relativeDir, displayName, sourcePath))
} catch (e: Exception) {
result.error("save_failed", e.message, null)
}
}
"readCbzToPath" -> {
val uriOrPath = call.argument<String>("uri")
val destPath = call.argument<String>("destPath")
if (uriOrPath == null || destPath == null) {
result.error("bad_args", "uri and destPath required", null)
return@setMethodCallHandler
}
try {
result.success(readCbzToPath(uriOrPath, destPath))
} catch (e: Exception) {
result.error("read_failed", e.message, null)
}
}
"deleteCbz" -> {
val uriOrPath = call.argument<String>("uri")
if (uriOrPath == null) {
result.error("bad_args", "uri required", null)
return@setMethodCallHandler
}
try {
result.success(deleteCbz(uriOrPath))
} catch (e: Exception) {
result.error("delete_failed", e.message, null)
}
}
"sdkInt" -> result.success(Build.VERSION.SDK_INT)
else -> result.notImplemented()
}
}
}
private fun saveCbzFromPath(
relativeDir: String,
displayName: String,
sourcePath: String,
): String {
val source = File(sourcePath)
if (!source.exists()) {
throw IllegalStateException("Source file missing: $sourcePath")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = applicationContext.contentResolver
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/vnd.comicbook+zip")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/$relativeDir",
)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
val uri = resolver.insert(collection, values)
?: throw IllegalStateException("MediaStore insert returned null")
resolver.openOutputStream(uri)?.use { out ->
FileInputStream(source).use { input -> input.copyTo(out) }
out.flush()
} ?: throw IllegalStateException("Could not open output stream for $uri")
values.clear()
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
resolver.update(uri, values, null, null)
return uri.toString()
}
val downloads =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val dir = File(downloads, relativeDir)
if (!dir.exists() && !dir.mkdirs()) {
throw IllegalStateException("Could not create ${dir.absolutePath}")
}
val file = File(dir, displayName)
FileInputStream(source).use { input ->
FileOutputStream(file).use { out ->
input.copyTo(out)
out.flush()
}
}
return file.absolutePath
}
private fun readCbzToPath(uriOrPath: String, destPath: String): String {
val dest = File(destPath)
dest.parentFile?.mkdirs()
if (uriOrPath.startsWith("content://")) {
val uri = Uri.parse(uriOrPath)
applicationContext.contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(dest).use { out ->
input.copyTo(out)
out.flush()
}
} ?: throw IllegalStateException("Could not open $uriOrPath")
} else {
File(uriOrPath).inputStream().use { input ->
FileOutputStream(dest).use { out ->
input.copyTo(out)
out.flush()
}
}
}
return dest.absolutePath
}
private fun deleteCbz(uriOrPath: String): Boolean {
return if (uriOrPath.startsWith("content://")) {
val uri = Uri.parse(uriOrPath)
applicationContext.contentResolver.delete(uri, null, null) > 0
} else {
val file = File(uriOrPath)
!file.exists() || file.delete()
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>