mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(oekaki): add drawing flow for /i/ (#1144)
* feat(oekaki): add drawing flow for /i/ * fix(oekaki): address review feedback * fix(oekaki): reset Tegaki edit sessions * fix(oekaki): block drawing during export * fix(oekaki): destroy Tegaki on preload errors * fix(oekaki): preserve drawing on export failure * fix(oekaki): unlock controls after export failure
This commit is contained in:
@@ -3,6 +3,7 @@ package fivechan.android;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
@@ -31,6 +32,7 @@ public class FileUploaderPlugin extends Plugin {
|
||||
|
||||
private static final String PROVIDER_CATBOX = "catbox";
|
||||
private static final long CATBOX_TIMEOUT_SEC = 30;
|
||||
private static final int MAX_GENERATED_UPLOAD_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
@PluginMethod
|
||||
public void pickAndUploadMedia(PluginCall call) {
|
||||
@@ -47,6 +49,62 @@ public class FileUploaderPlugin extends Plugin {
|
||||
startActivityForResult(call, intent, "pickFileResult");
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void uploadGeneratedMedia(PluginCall call) {
|
||||
Log.d(TAG, "uploadGeneratedMedia called");
|
||||
List<String> providerOrder = getProviderOrder(call);
|
||||
if (providerOrder.isEmpty()) {
|
||||
call.reject("No supported upload providers selected");
|
||||
return;
|
||||
}
|
||||
|
||||
String base64 = call.getString("base64");
|
||||
if (base64 == null || base64.trim().isEmpty()) {
|
||||
call.reject("Generated upload data is required");
|
||||
return;
|
||||
}
|
||||
int commaIndex = base64.indexOf(',');
|
||||
String base64Payload = commaIndex >= 0 ? base64.substring(commaIndex + 1) : base64;
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = Base64.decode(base64Payload, Base64.DEFAULT);
|
||||
} catch (IllegalArgumentException e) {
|
||||
call.reject("Generated upload data is invalid");
|
||||
return;
|
||||
}
|
||||
if (bytes.length == 0) {
|
||||
call.reject("Generated upload data is empty");
|
||||
return;
|
||||
}
|
||||
if (bytes.length > MAX_GENERATED_UPLOAD_BYTES) {
|
||||
call.reject("Generated upload is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
String fileName = call.getString("fileName", "tegaki.png");
|
||||
String mimeType = call.getString("mimeType", "application/octet-stream");
|
||||
new Thread(
|
||||
() -> {
|
||||
File cachedFile = null;
|
||||
try {
|
||||
cachedFile = FileUtils.writeBytesToCacheFile(getContext(), fileName, bytes);
|
||||
tryProvidersSequentially(Uri.fromFile(cachedFile), providerOrder, call, fileName, bytes, mimeType);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Generated upload failed", e);
|
||||
try {
|
||||
call.reject("Upload failed: " + e.getMessage());
|
||||
} catch (Exception rejectEx) {
|
||||
Log.e(TAG, "Failed to reject generated upload call", rejectEx);
|
||||
}
|
||||
} finally {
|
||||
if (cachedFile != null && cachedFile.exists() && !cachedFile.delete()) {
|
||||
Log.w(TAG, "Could not delete generated upload cache file: " + cachedFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
private List<String> parseProviderOrder(PluginCall call) {
|
||||
List<String> order = new ArrayList<>();
|
||||
JSArray arr = call.getArray("providerOrder");
|
||||
@@ -127,8 +185,19 @@ public class FileUploaderPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private void tryProvidersSequentially(Uri fileUri, List<String> providerOrder, PluginCall call) {
|
||||
tryProvidersSequentially(fileUri, providerOrder, call, null, null, null);
|
||||
}
|
||||
|
||||
private void tryProvidersSequentially(
|
||||
Uri fileUri,
|
||||
List<String> providerOrder,
|
||||
PluginCall call,
|
||||
String generatedFileName,
|
||||
byte[] generatedFileBytes,
|
||||
String generatedMimeType) {
|
||||
List<JSObject> attempts = new ArrayList<>();
|
||||
StringBuilder errorSummary = new StringBuilder();
|
||||
String fileName = generatedFileName != null ? generatedFileName : getFileName(fileUri);
|
||||
|
||||
for (String provider : providerOrder) {
|
||||
JSObject attempt = new JSObject();
|
||||
@@ -140,18 +209,21 @@ public class FileUploaderPlugin extends Plugin {
|
||||
if (res.success) {
|
||||
attempt.put("url", res.url);
|
||||
attempts.add(attempt);
|
||||
resolveWithSuccess(call, res.url, getFileName(fileUri), provider, attempts);
|
||||
resolveWithSuccess(call, res.url, fileName, provider, attempts);
|
||||
return;
|
||||
}
|
||||
attempt.put("error", res.error);
|
||||
errorSummary.append(provider).append(": ").append(res.error).append("; ");
|
||||
} else if (MediaUploadRecipes.isWebViewProvider(provider)) {
|
||||
MediaUploadResult res = uploadViaWebViewSync(fileUri, provider);
|
||||
MediaUploadResult res =
|
||||
generatedFileBytes != null
|
||||
? uploadGeneratedViaWebViewSync(generatedFileBytes, fileName, generatedMimeType, provider)
|
||||
: uploadViaWebViewSync(fileUri, provider);
|
||||
attempt.put("success", res.success);
|
||||
if (res.success) {
|
||||
attempt.put("url", res.url);
|
||||
attempts.add(attempt);
|
||||
resolveWithSuccess(call, res.url, getFileName(fileUri), provider, attempts);
|
||||
resolveWithSuccess(call, res.url, fileName, provider, attempts);
|
||||
return;
|
||||
}
|
||||
attempt.put("error", res.error);
|
||||
@@ -251,4 +323,54 @@ public class FileUploaderPlugin extends Plugin {
|
||||
return new MediaUploadResult(false, null, "Interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
private MediaUploadResult uploadGeneratedViaWebViewSync(
|
||||
byte[] fileBytes, String fileName, String mimeType, String provider) {
|
||||
JSObject statusUpdate = new JSObject();
|
||||
statusUpdate.put("status", "Uploading to " + provider + "...");
|
||||
notifyListeners("uploadStatus", statusUpdate);
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||
|
||||
AppCompatActivity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return new MediaUploadResult(false, null, "Activity unavailable");
|
||||
}
|
||||
|
||||
MediaUploadCallback callback =
|
||||
res -> {
|
||||
resultRef.set(res);
|
||||
latch.countDown();
|
||||
};
|
||||
|
||||
activity.runOnUiThread(
|
||||
() -> {
|
||||
MediaUploadAutomationRunner runner =
|
||||
new MediaUploadAutomationRunner(
|
||||
getContext(),
|
||||
fileBytes,
|
||||
fileName,
|
||||
mimeType,
|
||||
provider,
|
||||
callback,
|
||||
null);
|
||||
runner.run();
|
||||
});
|
||||
|
||||
try {
|
||||
boolean ok =
|
||||
latch.await(
|
||||
MediaUploadRecipes.getUploadTimeoutMs(provider) + 5000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
if (!ok) {
|
||||
return new MediaUploadResult(false, null, "WebView upload timeout");
|
||||
}
|
||||
MediaUploadResult r = resultRef.get();
|
||||
return r != null ? r : new MediaUploadResult(false, null, "No result");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return new MediaUploadResult(false, null, "Interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ public class FileUtils {
|
||||
private static final Pattern UNSAFE_CHARS = Pattern.compile("[^a-zA-Z0-9._-]");
|
||||
|
||||
public static File getFileFromUri(Context context, Uri uri) throws Exception {
|
||||
if ("file".equals(uri.getScheme()) && uri.getPath() != null) {
|
||||
File source = new File(uri.getPath());
|
||||
if (source.isFile()) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
String fileName = getFileName(context, uri);
|
||||
String sanitizedName = sanitizeFileName(fileName);
|
||||
File file = new File(context.getCacheDir(), sanitizedName);
|
||||
@@ -69,6 +76,25 @@ public class FileUtils {
|
||||
* Takes only the last path segment, removes/replaces unsafe chars, enforces max length,
|
||||
* and falls back to a UUID if the result is empty.
|
||||
*/
|
||||
public static File writeBytesToCacheFile(Context context, String fileName, byte[] bytes) throws Exception {
|
||||
String safeName = sanitizeFileName(fileName);
|
||||
int dot = safeName.lastIndexOf('.');
|
||||
String base = dot > 0 ? safeName.substring(0, dot) : safeName;
|
||||
String ext = dot > 0 ? safeName.substring(dot) : null;
|
||||
if (base.length() < 3) {
|
||||
base = (base + "___").substring(0, 3);
|
||||
}
|
||||
if (base.length() > 64) {
|
||||
base = base.substring(0, 64);
|
||||
}
|
||||
File file = File.createTempFile(base + "-", ext, context.getCacheDir());
|
||||
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(bytes);
|
||||
outputStream.flush();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private static String sanitizeFileName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return UUID.randomUUID().toString();
|
||||
@@ -82,4 +108,4 @@ public class FileUtils {
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user