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;