mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -38,6 +38,7 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -33,8 +36,4 @@
|
|||||||
android:resource="@xml/file_paths"></meta-data>
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"CapacitorHttp": {
|
"CapacitorHttp": {
|
||||||
"enabled": true
|
"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;
|
package plebchan.android;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
import com.getcapacitor.BridgeActivity;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,13 @@ const config: CapacitorConfig = {
|
|||||||
CapacitorHttp: {
|
CapacitorHttp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
FileUploader: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
androidScheme: 'https'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
@@ -44,7 +44,7 @@ const BoardHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.boardTitle}>
|
<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 && (
|
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && (
|
||||||
<span className={`${styles.offlineIcon} ${offlineIconClass}`} title={offlineTitle} />
|
<span className={`${styles.offlineIcon} ${offlineIconClass}`} title={offlineTitle} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.postFormTable button {
|
.postFormTable button {
|
||||||
padding: 4px 8px;
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,3 +140,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uploadButton button {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton span {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
@@ -14,6 +14,10 @@ import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
|||||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||||
import useAnonMode from '../../hooks/use-anon-mode';
|
import useAnonMode from '../../hooks/use-anon-mode';
|
||||||
import usePublishPostStore from '../../stores/use-publish-post-store';
|
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 }) => {
|
export const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -214,6 +218,36 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
}
|
}
|
||||||
}, [anonMode, getAnonAddressForPost, getAnonAddressForReply, isInPostView]);
|
}, [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 (
|
return (
|
||||||
<table className={styles.postFormTable}>
|
<table className={styles.postFormTable}>
|
||||||
<tbody>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{!isInPostView && (
|
{!isInPostView && (
|
||||||
@@ -266,6 +304,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
spellCheck='false'
|
spellCheck='false'
|
||||||
ref={urlRef}
|
ref={urlRef}
|
||||||
|
disabled={isUploading}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setUrl(e.target.value);
|
setUrl(e.target.value);
|
||||||
isInPostView ? setPublishReplyOptions({ link: e.target.value || undefined }) : setPublishPostStore({ link: e.target.value || undefined });
|
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>
|
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} />}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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}>
|
<tr className={styles.spoilerButton}>
|
||||||
<td>{t('options')}</td>
|
<td>{t('options')}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user