feat(android app): add 'choose file' button to auto upload media to catbox in the background

more free and open web hosting services can be added later
This commit is contained in:
Tom (plebeius.eth)
2024-11-02 18:42:52 +01:00
parent 3f4f0229ba
commit f40c2c2f88
11 changed files with 258 additions and 10 deletions
+1
View File
@@ -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"
+3 -4
View File
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@@ -33,8 +36,4 @@
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -6,6 +6,12 @@
"plugins": {
"CapacitorHttp": {
"enabled": true
},
"FileUploader": {
"enabled": true
}
},
"server": {
"androidScheme": "https"
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
}
+7 -1
View File
@@ -9,7 +9,13 @@ const config: CapacitorConfig = {
CapacitorHttp: {
enabled: true,
},
FileUploader: {
enabled: true
}
},
server: {
androidScheme: 'https'
}
};
export default config;
export default config;
+1 -1
View File
@@ -44,7 +44,7 @@ const BoardHeader = () => {
</div>
)}
<div className={styles.boardTitle}>
{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 && (
<span className={`${styles.offlineIcon} ${offlineIconClass}`} title={offlineTitle} />
)}
+12 -2
View File
@@ -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;
}
+51 -1
View File
@@ -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<string | null>(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 (
<table className={styles.postFormTable}>
<tbody>
@@ -233,7 +267,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
}}
/>
{isInPostView && <button onClick={onPublishReply}>{t('post')}</button>}
{isInPostView && (
<button onClick={onPublishReply} disabled={isUploading}>
{t('post')}
</button>
)}
</td>
</tr>
{!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:
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} />}</span>
</td>
</tr>
{isAndroid && (
<tr className={styles.uploadButton}>
<td>{t('file')}</td>
<td>
<button onClick={handleUpload} disabled={isUploading}>
{isUploading ? 'uploading...' : 'choose file'}
</button>
<span>{uploadedFileName ? uploadedFileName : 'No file chosen'}</span>
</td>
</tr>
)}
<tr className={styles.spoilerButton}>
<td>{t('options')}</td>
<td>
+9
View File
@@ -0,0 +1,9 @@
import { registerPlugin } from '@capacitor/core';
export interface FileUploaderPlugin {
pickAndUploadMedia(): Promise<{ url: string; fileName: string }>;
}
const FileUploader = registerPlugin<FileUploaderPlugin>('FileUploader');
export default FileUploader;