diff --git a/android/app/build.gradle b/android/app/build.gradle
index cf186053..0e40d160 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -38,6 +38,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android')
implementation project(':capacitor-cordova-android-plugins')
+ implementation 'com.squareup.okhttp3:okhttp:4.11.0'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9e8e1d95..6ffe9d38 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,8 @@
+
+
+
-
-
-
-
diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json
index 24d6ee89..1a1b59c1 100644
--- a/android/app/src/main/assets/capacitor.config.json
+++ b/android/app/src/main/assets/capacitor.config.json
@@ -6,6 +6,12 @@
"plugins": {
"CapacitorHttp": {
"enabled": true
+ },
+ "FileUploader": {
+ "enabled": true
}
+ },
+ "server": {
+ "androidScheme": "https"
}
}
diff --git a/android/app/src/main/java/plebchan/android/FileUploaderPlugin.java b/android/app/src/main/java/plebchan/android/FileUploaderPlugin.java
new file mode 100644
index 00000000..b633dec1
--- /dev/null
+++ b/android/app/src/main/java/plebchan/android/FileUploaderPlugin.java
@@ -0,0 +1,110 @@
+package plebchan.android;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import com.getcapacitor.JSObject;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.ActivityCallback;
+import com.getcapacitor.PluginCall;
+import androidx.activity.result.ActivityResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+@CapacitorPlugin(name = "FileUploader")
+public class FileUploaderPlugin extends Plugin {
+ private static final String TAG = "FileUploaderPlugin";
+
+ @PluginMethod
+ public void pickAndUploadMedia(PluginCall call) {
+ Log.d(TAG, "pickAndUploadMedia called");
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ String[] mimeTypes = {"image/jpeg", "image/png", "video/mp4", "video/webm"};
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+ startActivityForResult(call, intent, "pickFileResult");
+ }
+
+ @ActivityCallback
+ private void pickFileResult(PluginCall call, ActivityResult result) {
+ Log.d(TAG, "pickFileResult callback received");
+ if (call == null) {
+ return;
+ }
+
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ Intent data = result.getData();
+ if (data != null) {
+ Uri uri = data.getData();
+ uploadToCatbox(uri, call);
+ } else {
+ call.reject("No data received");
+ }
+ } else {
+ call.reject("File selection cancelled");
+ }
+ }
+
+ private void uploadToCatbox(Uri fileUri, PluginCall call) {
+ new Thread(() -> {
+ try {
+ Log.d(TAG, "Starting file conversion from URI");
+ File file = FileUtils.getFileFromUri(getContext(), fileUri);
+ Log.d(TAG, "File name: " + file.getName());
+
+ JSObject statusUpdate = new JSObject();
+ statusUpdate.put("status", "Uploading to catbox.moe...");
+ notifyListeners("uploadStatus", statusUpdate);
+
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .build();
+
+ RequestBody requestBody = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("reqtype", "fileupload")
+ .addFormDataPart("fileToUpload", file.getName(),
+ RequestBody.create(MediaType.parse("application/octet-stream"), file))
+ .build();
+
+ Request request = new Request.Builder()
+ .url("https://catbox.moe/user/api.php")
+ .post(requestBody)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) throw new IOException("Unexpected response " + response);
+
+ String url = response.body().string();
+ Log.d(TAG, "Upload successful. URL: " + url);
+
+ JSObject ret = new JSObject();
+ ret.put("url", url);
+ ret.put("fileName", file.getName());
+ ret.put("status", "Upload complete!");
+ call.resolve(ret);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Upload failed", e);
+ call.reject("Upload failed: " + e.getMessage());
+ }
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/plebchan/android/FileUtils.java b/android/app/src/main/java/plebchan/android/FileUtils.java
new file mode 100644
index 00000000..9abf1200
--- /dev/null
+++ b/android/app/src/main/java/plebchan/android/FileUtils.java
@@ -0,0 +1,50 @@
+package plebchan.android;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+
+public class FileUtils {
+ public static File getFileFromUri(Context context, Uri uri) throws Exception {
+ String fileName = getFileName(context, uri);
+ File file = new File(context.getCacheDir(), fileName);
+
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ FileOutputStream outputStream = new FileOutputStream(file)) {
+ byte[] buffer = new byte[4096];
+ int length;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+ outputStream.flush();
+ return file;
+ }
+ }
+
+ private static String getFileName(Context context, Uri uri) {
+ String result = null;
+ if (uri.getScheme().equals("content")) {
+ try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (columnIndex != -1) {
+ result = cursor.getString(columnIndex);
+ }
+ }
+ }
+ }
+ if (result == null) {
+ result = uri.getPath();
+ int cut = result.lastIndexOf('/');
+ if (cut != -1) {
+ result = result.substring(cut + 1);
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/plebchan/android/MainActivity.java b/android/app/src/main/java/plebchan/android/MainActivity.java
index 8a06a3f9..f1a9f03f 100644
--- a/android/app/src/main/java/plebchan/android/MainActivity.java
+++ b/android/app/src/main/java/plebchan/android/MainActivity.java
@@ -1,5 +1,12 @@
package plebchan.android;
+import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
-public class MainActivity extends BridgeActivity {}
+public class MainActivity extends BridgeActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ registerPlugin(FileUploaderPlugin.class);
+ super.onCreate(savedInstanceState);
+ }
+}
\ No newline at end of file
diff --git a/capacitor.config.ts b/capacitor.config.ts
index c34188dd..ab5056bb 100644
--- a/capacitor.config.ts
+++ b/capacitor.config.ts
@@ -9,7 +9,13 @@ const config: CapacitorConfig = {
CapacitorHttp: {
enabled: true,
},
+ FileUploader: {
+ enabled: true
+ }
},
+ server: {
+ androidScheme: 'https'
+ }
};
-export default config;
+export default config;
\ No newline at end of file
diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx
index b25f7696..d98b4f54 100644
--- a/src/components/board-header/board-header.tsx
+++ b/src/components/board-header/board-header.tsx
@@ -44,7 +44,7 @@ const BoardHeader = () => {
)}
- {title || shortAddress ? (shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress) : subplebbitAddress}
+ {title || (shortAddress ? (shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress) : subplebbitAddress)}
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && (
)}
diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css
index 2124f76a..d0f52f2d 100644
--- a/src/components/post-form/post-form.module.css
+++ b/src/components/post-form/post-form.module.css
@@ -125,13 +125,13 @@
.postFormTable td input {
height: 20px;
}
-
+
.postFormTable td textarea {
height: 80px;
}
.postFormTable button {
- padding: 4px 8px;
+ padding: 3px 6px;
}
}
@@ -139,4 +139,14 @@
.postFormMobile {
display: none;
}
+}
+
+.uploadButton button {
+ position: relative;
+ margin: 0;
+ margin-right: 5px;
+}
+
+.uploadButton span {
+ font-weight: normal;
}
\ No newline at end of file
diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx
index 97513fc0..71cbc4d6 100644
--- a/src/components/post-form/post-form.tsx
+++ b/src/components/post-form/post-form.tsx
@@ -14,6 +14,10 @@ import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useAnonMode from '../../hooks/use-anon-mode';
import usePublishPostStore from '../../stores/use-publish-post-store';
+import FileUploader from '../../plugins/file-uploader';
+import { Capacitor } from '@capacitor/core';
+
+const isAndroid = Capacitor.getPlatform() === 'android';
export const LinkTypePreviewer = ({ link }: { link: string }) => {
const { t } = useTranslation();
@@ -214,6 +218,36 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
}, [anonMode, getAnonAddressForPost, getAnonAddressForReply, isInPostView]);
+ // on android, auto upload file to image hosting sites with open api
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadedFileName, setUploadedFileName] = useState
(null);
+ const handleUpload = async () => {
+ try {
+ setIsUploading(true);
+ const result = await FileUploader.pickAndUploadMedia();
+ console.log('Upload result:', result);
+ if (result.url) {
+ setUrl(result.url);
+ if (urlRef.current) {
+ urlRef.current.value = result.url;
+ }
+ isInPostView ? setPublishReplyOptions({ link: result.url || undefined }) : setPublishPostStore({ link: result.url || undefined });
+ if (result.fileName) {
+ setUploadedFileName(result.fileName);
+ }
+ }
+ } catch (error) {
+ console.error('Upload failed:', error);
+ if (error instanceof Error && error.message !== 'File selection cancelled') {
+ alert(`${t('upload_failed')}: ${error.message}`);
+ } else if (typeof error === 'string' && error !== 'File selection cancelled') {
+ alert(`${t('upload_failed')}: ${error}`);
+ }
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
return (
@@ -233,7 +267,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
}}
/>
- {isInPostView && }
+ {isInPostView && (
+
+ )}
{!isInPostView && (
@@ -266,6 +304,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
autoComplete='off'
spellCheck='false'
ref={urlRef}
+ disabled={isUploading}
onChange={(e) => {
setUrl(e.target.value);
isInPostView ? setPublishReplyOptions({ link: e.target.value || undefined }) : setPublishPostStore({ link: e.target.value || undefined });
@@ -274,6 +313,17 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
{url && }
+ {isAndroid && (
+
+ | {t('file')} |
+
+
+ {uploadedFileName ? uploadedFileName : 'No file chosen'}
+ |
+
+ )}
| {t('options')} |
diff --git a/src/plugins/file-uploader.ts b/src/plugins/file-uploader.ts
new file mode 100644
index 00000000..f1627b3c
--- /dev/null
+++ b/src/plugins/file-uploader.ts
@@ -0,0 +1,9 @@
+import { registerPlugin } from '@capacitor/core';
+
+export interface FileUploaderPlugin {
+ pickAndUploadMedia(): Promise<{ url: string; fileName: string }>;
+}
+
+const FileUploader = registerPlugin('FileUploader');
+
+export default FileUploader;
|