mirror of
https://github.com/plebbit/seedit.git
synced 2026-04-27 10:40:22 -04:00
feat(submit page): add media file upload
On desktop or android, the user can drop or select a media file in the upload box, which automatically uploads it to catbox.moe in the background, and pastes the direct link to the media in the url field.
This commit is contained in:
@@ -42,6 +42,8 @@ dependencies {
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation "org.apache.cordova:framework:$cordovaAndroidVersion"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.okio:okio:3.9.0'
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"plugins": {
|
||||
"CapacitorHttp": {
|
||||
"enabled": true
|
||||
},
|
||||
"FileUploader": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"androidScheme": "https"
|
||||
}
|
||||
}
|
||||
|
||||
268
android/app/src/main/java/seedit/android/FileUploaderPlugin.java
Normal file
268
android/app/src/main/java/seedit/android/FileUploaderPlugin.java
Normal file
@@ -0,0 +1,268 @@
|
||||
package seedit.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.database.Cursor;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
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.io.FileOutputStream;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.io.InputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void uploadMedia(PluginCall call) {
|
||||
if (!call.getData().has("fileData") || !call.getData().has("fileName")) {
|
||||
call.reject("Missing required parameters fileData or fileName");
|
||||
return;
|
||||
}
|
||||
|
||||
String fileData = call.getString("fileData");
|
||||
String fileName = call.getString("fileName");
|
||||
|
||||
// Execute in background thread to avoid blocking UI
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// Convert base64 to file
|
||||
byte[] decodedBytes = android.util.Base64.decode(fileData, android.util.Base64.DEFAULT);
|
||||
File tempFile = new File(getContext().getCacheDir(), fileName);
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||||
fos.write(decodedBytes);
|
||||
}
|
||||
|
||||
// Create multipart upload form
|
||||
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", fileName,
|
||||
RequestBody.create(MediaType.parse("application/octet-stream"), tempFile))
|
||||
.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();
|
||||
|
||||
// Return the result to JavaScript
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("url", url);
|
||||
ret.put("fileName", fileName);
|
||||
getActivity().runOnUiThread(() -> {
|
||||
call.resolve(ret);
|
||||
});
|
||||
|
||||
// Clean up temp file
|
||||
tempFile.delete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
call.reject("Upload failed: " + e.getMessage(), e);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pickMedia(PluginCall call) {
|
||||
Log.d(TAG, "pickMedia 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, "pickMediaResult");
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
private void pickMediaResult(PluginCall call, ActivityResult result) {
|
||||
Log.d(TAG, "pickMediaResult callback received");
|
||||
if (call == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
try {
|
||||
// Get file name
|
||||
String fileName = getFileNameFromUri(uri);
|
||||
|
||||
// Get mime type
|
||||
String mimeType = getContext().getContentResolver().getType(uri);
|
||||
if (mimeType == null) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
// Read file into a byte array
|
||||
InputStream inputStream = getContext().getContentResolver().openInputStream(uri);
|
||||
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) != -1) {
|
||||
byteBuffer.write(buffer, 0, len);
|
||||
}
|
||||
byte[] fileBytes = byteBuffer.toByteArray();
|
||||
inputStream.close();
|
||||
|
||||
// Convert to base64
|
||||
String base64Data = android.util.Base64.encodeToString(fileBytes, android.util.Base64.DEFAULT);
|
||||
|
||||
// Return data to JavaScript
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("data", base64Data);
|
||||
ret.put("fileName", fileName);
|
||||
ret.put("mimeType", mimeType);
|
||||
call.resolve(ret);
|
||||
} catch (Exception e) {
|
||||
call.reject("Failed to read file: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
call.reject("No data received");
|
||||
}
|
||||
} else {
|
||||
call.reject("File selection cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get file name from URI
|
||||
private String getFileNameFromUri(Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0) {
|
||||
result = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.getPath();
|
||||
int cut = result.lastIndexOf('/');
|
||||
if (cut != -1) {
|
||||
result = result.substring(cut + 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
50
android/app/src/main/java/seedit/android/FileUtils.java
Normal file
50
android/app/src/main/java/seedit/android/FileUtils.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package seedit.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 seedit.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,8 +7,8 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath 'com.google.gms:google-services:4.3.5'
|
||||
classpath 'com.android.tools.build:gradle:8.7.0'
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -22,3 +22,6 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
@@ -8,7 +8,10 @@ const config: CapacitorConfig = {
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
FileUploader: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
androidScheme: 'https'
|
||||
|
||||
129
electron/main.js
129
electron/main.js
@@ -1,15 +1,21 @@
|
||||
import './log.js';
|
||||
import { app, BrowserWindow, Menu, MenuItem, Tray, screen as electronScreen, shell, dialog, nativeTheme, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow, Menu, MenuItem, Tray, shell, dialog, nativeTheme, ipcMain } from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import EnvPaths from 'env-paths';
|
||||
import startIpfs from './start-ipfs.js';
|
||||
import './start-plebbit-rpc.js';
|
||||
import { URL, fileURLToPath } from 'node:url';
|
||||
import { URL } from 'node:url';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
import packageJson from '../package.json' with { type: 'json' };
|
||||
const dirname = path.join(path.dirname(fileURLToPath(import.meta.url)));
|
||||
import FormData from 'form-data';
|
||||
import fetch from 'node-fetch';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(__filename);
|
||||
|
||||
let startIpfsError;
|
||||
startIpfs.onError = (error) => {
|
||||
@@ -78,7 +84,8 @@ const createMainWindow = () => {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: true, // TODO: change to isDev when no bugs left
|
||||
preload: path.join(dirname, 'preload.js'),
|
||||
preload: path.join(dirname, 'preload.mjs'),
|
||||
sandbox: false, // Required for ESM preload scripts
|
||||
},
|
||||
});
|
||||
|
||||
@@ -316,3 +323,117 @@ app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Setup FileUploader plugin
|
||||
ipcMain.handle('plugin:file-uploader:pickAndUploadMedia', async (event) => {
|
||||
try {
|
||||
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Images & Videos', extensions: ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'webm'] }],
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
throw new Error('File selection cancelled');
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0];
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Create form data for upload
|
||||
const formData = new FormData();
|
||||
formData.append('reqtype', 'fileupload');
|
||||
formData.append('fileToUpload', createReadStream(filePath));
|
||||
|
||||
// Upload to catbox.moe
|
||||
const response = await fetch('https://catbox.moe/user/api.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const url = await response.text();
|
||||
return { url, fileName };
|
||||
} catch (error) {
|
||||
console.error('FileUploader error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('plugin:file-uploader:uploadMedia', async (event, fileData) => {
|
||||
try {
|
||||
console.log('uploadMedia handler called with data:', typeof fileData);
|
||||
|
||||
// Create form data for upload
|
||||
const formData = new FormData();
|
||||
formData.append('reqtype', 'fileupload');
|
||||
|
||||
// Handle different types of inputs
|
||||
if (fileData.fileData && fileData.fileName) {
|
||||
// Convert base64 to buffer
|
||||
const buffer = Buffer.from(fileData.fileData, 'base64');
|
||||
formData.append('fileToUpload', buffer, fileData.fileName);
|
||||
} else {
|
||||
throw new Error('Invalid file data');
|
||||
}
|
||||
|
||||
// Upload to catbox.moe
|
||||
const response = await fetch('https://catbox.moe/user/api.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const url = await response.text();
|
||||
return { url, fileName: fileData.fileName || 'uploaded-file' };
|
||||
} catch (error) {
|
||||
console.error('FileUploader uploadMedia error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the pickMedia handler
|
||||
ipcMain.handle('plugin:file-uploader:pickMedia', async (event) => {
|
||||
try {
|
||||
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Images & Videos', extensions: ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'webm'] }],
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
throw new Error('File selection cancelled');
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0];
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Read the file as base64
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
// Determine mime type from extension
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
let mimeType = 'application/octet-stream';
|
||||
if (ext === '.jpg' || ext === '.jpeg') mimeType = 'image/jpeg';
|
||||
else if (ext === '.png') mimeType = 'image/png';
|
||||
else if (ext === '.gif') mimeType = 'image/gif';
|
||||
else if (ext === '.mp4') mimeType = 'video/mp4';
|
||||
else if (ext === '.webm') mimeType = 'video/webm';
|
||||
|
||||
return {
|
||||
data: base64Data,
|
||||
fileName,
|
||||
mimeType,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('FileUploader pickMedia error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(__filename);
|
||||
|
||||
// dev uses http://localhost, prod uses file://...index.html
|
||||
const isDev = window.location.protocol === 'http:';
|
||||
@@ -17,3 +22,19 @@ ipcRenderer.send('get-plebbit-rpc-auth-key');
|
||||
|
||||
// uncomment for logs
|
||||
// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,seedit:*'
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
invoke: (channel, ...args) => {
|
||||
const validChannels = [
|
||||
'plugin:file-uploader:pickAndUploadMedia',
|
||||
'plugin:file-uploader:uploadMedia',
|
||||
'plugin:file-uploader:pickMedia'
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
throw new Error(`Unauthorized IPC channel: ${channel}`);
|
||||
}
|
||||
});
|
||||
60
electron/src/main.ts
Normal file
60
electron/src/main.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { setupFileUploaderPlugin } from './plugins/file-uploader';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
// Prevent file drops on the window
|
||||
mainWindow.webContents.on('will-navigate', (e, url) => {
|
||||
if (url !== mainWindow.webContents.getURL()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent default file drop behavior
|
||||
mainWindow.webContents.on('drop', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Setup the FileUploader plugin
|
||||
setupFileUploaderPlugin(mainWindow);
|
||||
|
||||
// Load your app
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:3000');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../build/index.html'));
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
128
electron/src/plugins/file-uploader.ts
Normal file
128
electron/src/plugins/file-uploader.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { dialog, ipcMain } from 'electron';
|
||||
import { createReadStream } from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { app } from 'electron';
|
||||
|
||||
export function setupFileUploaderPlugin(mainWindow: Electron.BrowserWindow) {
|
||||
// Handle the file upload request from the renderer
|
||||
ipcMain.handle('plugin:file-uploader:pickAndUploadMedia', async () => {
|
||||
try {
|
||||
// Open file dialog
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Images & Videos', extensions: ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'webm'] }],
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
throw new Error('File selection cancelled');
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0];
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Create form data for upload
|
||||
const formData = new FormData();
|
||||
formData.append('reqtype', 'fileupload');
|
||||
formData.append('fileToUpload', createReadStream(filePath));
|
||||
|
||||
// Upload to catbox.moe
|
||||
const response = await fetch('https://catbox.moe/user/api.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const url = await response.text();
|
||||
return { url, fileName };
|
||||
} catch (error) {
|
||||
console.error('FileUploader error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the uploadMedia handler
|
||||
ipcMain.handle('plugin:file-uploader:uploadMedia', async (_, fileData) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('reqtype', 'fileupload');
|
||||
|
||||
if (fileData.fileData && fileData.fileName) {
|
||||
// If we have base64 data from pickMedia
|
||||
const tempFilePath = path.join(app.getPath('temp'), fileData.fileName);
|
||||
|
||||
// Write the base64 data to a temp file
|
||||
fs.writeFileSync(tempFilePath, Buffer.from(fileData.fileData, 'base64'));
|
||||
|
||||
// Append the file to the form
|
||||
formData.append('fileToUpload', createReadStream(tempFilePath));
|
||||
|
||||
// Upload to catbox.moe
|
||||
const response = await fetch('https://catbox.moe/user/api.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
// Clean up temp file
|
||||
fs.unlinkSync(tempFilePath);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const url = await response.text();
|
||||
return { url, fileName: fileData.fileName };
|
||||
} else {
|
||||
throw new Error('Invalid file data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('FileUploader uploadMedia error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the pickMedia handler to Electron
|
||||
ipcMain.handle('plugin:file-uploader:pickMedia', async () => {
|
||||
try {
|
||||
// Open file dialog
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Images & Videos', extensions: ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'webm'] }],
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
throw new Error('File selection cancelled');
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0];
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Read the file as base64
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
// Determine mime type from extension
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
let mimeType = 'application/octet-stream';
|
||||
if (ext === '.jpg' || ext === '.jpeg') mimeType = 'image/jpeg';
|
||||
else if (ext === '.png') mimeType = 'image/png';
|
||||
else if (ext === '.gif') mimeType = 'image/gif';
|
||||
else if (ext === '.mp4') mimeType = 'video/mp4';
|
||||
else if (ext === '.webm') mimeType = 'video/webm';
|
||||
|
||||
return {
|
||||
data: base64Data,
|
||||
fileName,
|
||||
mimeType,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('FileUploader pickMedia error:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
14
electron/src/preload.ts
Normal file
14
electron/src/preload.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
invoke: (channel: string, ...args: any[]) => {
|
||||
// whitelist channels
|
||||
const validChannels = ['plugin:file-uploader:pickAndUploadMedia', 'plugin:file-uploader:uploadMedia', 'plugin:file-uploader:pickMedia'];
|
||||
if (validChannels.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
throw new Error(`Unauthorized IPC channel: ${channel}`);
|
||||
},
|
||||
});
|
||||
@@ -21,15 +21,18 @@
|
||||
"electron-context-menu": "3.3.0",
|
||||
"electron-is-dev": "2.0.0",
|
||||
"ext-name": "5.0.0",
|
||||
"form-data": "4.0.2",
|
||||
"i18next": "23.5.1",
|
||||
"i18next-browser-languagedetector": "7.1.0",
|
||||
"i18next-http-backend": "2.2.2",
|
||||
"json-stringify-pretty-compact": "4.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"memoizee": "0.4.15",
|
||||
"node-fetch": "2",
|
||||
"prettier": "3.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dropzone": "14.3.8",
|
||||
"react-i18next": "13.2.2",
|
||||
"react-markdown": "8.0.6",
|
||||
"react-router-dom": "6.16.0",
|
||||
@@ -84,6 +87,7 @@
|
||||
"@capacitor/core": "5.0.0",
|
||||
"@electron/rebuild": "3.6.0",
|
||||
"@types/memoizee": "0.4.9",
|
||||
"@types/node-fetch": "2",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
|
||||
BIN
public/assets/camera.png
Normal file
BIN
public/assets/camera.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/assets/upload.png
Normal file
BIN
public/assets/upload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
52
src/plugins/file-uploader.ts
Normal file
52
src/plugins/file-uploader.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
export interface FileUploaderPlugin {
|
||||
pickAndUploadMedia(): Promise<{ url: string; fileName: string }>;
|
||||
uploadMedia(fileData: { fileData?: string; fileName: string } | File): Promise<{ url: string; fileName: string }>;
|
||||
pickMedia(): Promise<{ data: string; fileName: string; mimeType: string }>;
|
||||
}
|
||||
|
||||
const FileUploader = Capacitor.isNativePlatform()
|
||||
? registerPlugin<FileUploaderPlugin>('FileUploader')
|
||||
: {
|
||||
pickAndUploadMedia: async () => {
|
||||
// For Electron, we'll use IPC
|
||||
if (window.electron) {
|
||||
return window.electron.invoke('plugin:file-uploader:pickAndUploadMedia');
|
||||
}
|
||||
throw new Error('FileUploader is not supported on this platform');
|
||||
},
|
||||
uploadMedia: async (fileData: { fileData?: string; fileName: string } | File) => {
|
||||
if (window.electron) {
|
||||
return window.electron.invoke('plugin:file-uploader:uploadMedia', fileData);
|
||||
}
|
||||
throw new Error('FileUploader is not supported on this platform');
|
||||
},
|
||||
pickMedia: async () => {
|
||||
console.log('Calling pickMedia');
|
||||
if (window.electron) {
|
||||
console.log('window.electron exists, invoking IPC');
|
||||
try {
|
||||
const result = await window.electron.invoke('plugin:file-uploader:pickMedia');
|
||||
console.log('IPC result:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('IPC error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new Error('FileUploader is not supported on this platform');
|
||||
},
|
||||
};
|
||||
|
||||
export default FileUploader;
|
||||
|
||||
// Add TypeScript type declaration for Electron
|
||||
declare global {
|
||||
interface Window {
|
||||
electron?: {
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,8 @@
|
||||
--thumbnail-icon-nsfw: url('/assets/thumbnail-icon-nsfw-dark.png');
|
||||
--thumbnail-icon-spoiler: url('/assets/thumbnail-icon-spoiler-dark.png');
|
||||
--thumbnail-icon-text: url('/assets/thumbnail-icon-text-dark.png');
|
||||
--upload-box-background: rgb(15, 15, 15);
|
||||
--upload-box-border: 2px dashed #404649;
|
||||
--yellow: rgb(200, 171, 0);
|
||||
--yellow-box-background: rgb(56, 45, 0);
|
||||
--yellow-box-contrast: rgb(163, 130, 0);
|
||||
@@ -162,6 +164,8 @@
|
||||
--thumbnail-icon-nsfw: url('/assets/thumbnail-icon-nsfw.png');
|
||||
--thumbnail-icon-spoiler: url('/assets/thumbnail-icon-spoiler.png');
|
||||
--thumbnail-icon-text: url('/assets/thumbnail-icon-text.png');
|
||||
--upload-box-background: #fafafa;
|
||||
--upload-box-border: 2px dashed #bbb;
|
||||
--yellow: goldenrod;
|
||||
--yellow-box-background: #fff7d7;
|
||||
--yellow-box-contrast: #ffd634;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.content {
|
||||
margin: 7px 5px 50px 5px;
|
||||
color: var(--text);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.infobar {
|
||||
@@ -189,6 +190,22 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.urlCancelButton {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
background: var(--gray-light);
|
||||
line-height: 18px;
|
||||
font-size: 11px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logoError {
|
||||
padding-left: 10px;
|
||||
}
|
||||
@@ -232,4 +249,99 @@
|
||||
.input {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploadBox {
|
||||
position: relative;
|
||||
content: '';
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 10px;
|
||||
border: var(--upload-box-border);
|
||||
background-color: var(--upload-box-background);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.uploadBox.dragging {
|
||||
border-width: 5px;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cameraIcon {
|
||||
background-image: url('/assets/camera.png');
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 40px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cameraIcon {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropText {
|
||||
display: inline;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dropText {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.fileUploadIcon {
|
||||
background-image: url('/assets/upload.png');
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
margin-right: 5px;
|
||||
background-size: contain;
|
||||
pointer-events: none;
|
||||
vertical-align: middle;
|
||||
background-repeat: no-repeat;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.uploadBox label {
|
||||
color: var(--over18-alert-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
padding: 5px 12px 3px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
margin: 3px 5px;
|
||||
background-color: var(--button-background-color);
|
||||
border-bottom: 2px solid var(--button-border-bottom);
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.uploadBox label:hover {
|
||||
background-color: var(--button-background-color-hover);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import { useAccount, usePublishComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import usePublishPostStore from '../../stores/use-publish-post-store';
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
import { getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
import Embed from '../../components/post/embed';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import Markdown from '../../components/markdown';
|
||||
import FileUploader from '../../plugins/file-uploader';
|
||||
import styles from './submit-page.module.css';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
|
||||
const UrlField = () => {
|
||||
const UrlField = ({ url, setUrl, urlRef }: { url: string; setUrl: (url: string) => void; urlRef: React.RefObject<HTMLInputElement> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { setPublishPostStore } = usePublishPostStore();
|
||||
const [mediaError, setMediaError] = useState(false);
|
||||
const [url, setUrl] = useState('');
|
||||
|
||||
const mediaInfo = getLinkMediaInfo(url);
|
||||
const mediaType = mediaInfo?.type;
|
||||
@@ -46,7 +49,13 @@ const UrlField = () => {
|
||||
</>
|
||||
)}
|
||||
<div className={styles.boxContent}>
|
||||
{url && (
|
||||
<span className={styles.urlCancelButton} onClick={() => setUrl('')}>
|
||||
x
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
ref={urlRef}
|
||||
className={`${styles.input} ${styles.inputUrl}`}
|
||||
type='text'
|
||||
value={url ?? ''}
|
||||
@@ -69,6 +78,9 @@ const UrlField = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const isAndroid = Capacitor.getPlatform() === 'android';
|
||||
const isElectron = window.isElectron === true;
|
||||
|
||||
const Submit = () => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
@@ -77,6 +89,8 @@ const Submit = () => {
|
||||
|
||||
const [inputAddress, setInputAddress] = useState(params.subplebbitAddress || '');
|
||||
const [selectedSubplebbit, setSelectedSubplebbit] = useState(params.subplebbitAddress || '');
|
||||
const [url, setUrl] = useState('');
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -162,14 +176,14 @@ const Submit = () => {
|
||||
if (activeDropdownIndex !== -1) {
|
||||
const selectedAddress = filteredSubplebbitAddresses[activeDropdownIndex];
|
||||
setSelectedSubplebbit(selectedAddress);
|
||||
setPublishPostStore({ subplebbitAddress: selectedAddress });
|
||||
setSubmitStoreHook({ subplebbitAddress: selectedAddress });
|
||||
setInputAddress(selectedAddress);
|
||||
}
|
||||
setActiveDropdownIndex(-1);
|
||||
setIsInputAddressFocused(false);
|
||||
}
|
||||
},
|
||||
[filteredSubplebbitAddresses, activeDropdownIndex, setPublishPostStore],
|
||||
[filteredSubplebbitAddresses, activeDropdownIndex, setSubmitStoreHook],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -217,11 +231,130 @@ const Submit = () => {
|
||||
const handleSubplebbitSelect = (subplebbitAddress: string) => {
|
||||
setSelectedSubplebbit(subplebbitAddress);
|
||||
setInputAddress(subplebbitAddress);
|
||||
setPublishPostStore({ subplebbitAddress: subplebbitAddress });
|
||||
setSubmitStoreHook({ subplebbitAddress: subplebbitAddress });
|
||||
setIsInputAddressFocused(false);
|
||||
setActiveDropdownIndex(-1);
|
||||
};
|
||||
|
||||
// on android or electron, auto upload file to image hosting sites with open api
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isChoosingFile, setIsChoosingFile] = useState(false);
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
if (!(isAndroid || isElectron)) {
|
||||
if (window.confirm('This feature is only available on Seedit Android app, or desktop app (win/mac/linux) versions.\n\nGo to download links page on GitHub?')) {
|
||||
const link = document.createElement('a');
|
||||
link.href = 'https://github.com/plebbit/seedit/releases/latest';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
try {
|
||||
setIsChoosingFile(false);
|
||||
setIsUploading(true);
|
||||
|
||||
// for Electron, we need to convert the File to a format that can be sent via IPC
|
||||
if (isElectron) {
|
||||
const file = acceptedFiles[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
const fileData = await new Promise((resolve, reject) => {
|
||||
reader.onload = () => {
|
||||
const base64data = reader.result?.toString().split(',')[1];
|
||||
resolve({
|
||||
fileData: base64data,
|
||||
fileName: file.name,
|
||||
});
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
const result = await FileUploader.uploadMedia(fileData as { fileData?: string; fileName: string });
|
||||
if (result.url) {
|
||||
setUrl(result.url);
|
||||
setPublishPostStore({ link: result.url || undefined });
|
||||
}
|
||||
} else if (isAndroid) {
|
||||
// android can handle File objects directly
|
||||
const result = await FileUploader.uploadMedia(acceptedFiles[0]);
|
||||
if (result.url) {
|
||||
setUrl(result.url);
|
||||
setPublishPostStore({ link: result.url || undefined });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
if (error instanceof Error && !error.message.includes('File selection cancelled')) {
|
||||
alert(`${t('upload_failed')}: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setUrl, setPublishPostStore, t],
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
accept: {
|
||||
'image/*': [],
|
||||
'video/*': [],
|
||||
'audio/*': [],
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!(isAndroid || isElectron)) {
|
||||
if (window.confirm('This feature is only available on Seedit Android app, or desktop app (win/mac/linux) versions.\n\nGo to download links page on GitHub?')) {
|
||||
const link = document.createElement('a');
|
||||
link.href = 'https://github.com/plebbit/seedit/releases/latest';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsChoosingFile(true);
|
||||
|
||||
const pickedFile = await FileUploader.pickMedia(); // base64 data
|
||||
setIsChoosingFile(false);
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
const uploadResult = await FileUploader.uploadMedia({
|
||||
fileData: pickedFile.data,
|
||||
fileName: pickedFile.fileName,
|
||||
});
|
||||
|
||||
if (uploadResult?.url) {
|
||||
setUrl(uploadResult.url);
|
||||
setPublishPostStore({ link: uploadResult.url });
|
||||
} else {
|
||||
throw new Error('No URL returned from upload');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Process failed:', error);
|
||||
if (error instanceof Error && !error.message.includes('File selection cancelled')) {
|
||||
alert(`${t('upload_failed')}: ${error.message}`);
|
||||
} else if (typeof error === 'string' && !error.includes('File selection cancelled')) {
|
||||
alert(`${t('upload_failed')}: ${error}`);
|
||||
}
|
||||
} finally {
|
||||
setIsChoosingFile(false);
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<h1>
|
||||
@@ -231,17 +364,37 @@ const Submit = () => {
|
||||
values={{
|
||||
link: selectedSubplebbitData?.title || selectedSubplebbitData?.shortAddress || 'seedit',
|
||||
}}
|
||||
components={{
|
||||
1: selectedSubplebbitData?.shortAddress ? <Link to={`/p/${selectedSubplebbit}`} className={styles.location} /> : <></>,
|
||||
}}
|
||||
components={[selectedSubplebbitData?.shortAddress ? <Link key='link' to={`/p/${selectedSubplebbit}`} className={styles.location} /> : <span key='link' />]}
|
||||
/>
|
||||
</h1>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formContent}>
|
||||
{isOffline && selectedSubplebbit && <div className={styles.infobar}>{offlineTitle}</div>}
|
||||
<div className={styles.box}>
|
||||
<UrlField />
|
||||
<UrlField url={url} setUrl={setUrl} urlRef={urlRef} />
|
||||
</div>
|
||||
{url.length === 0 && (
|
||||
<div className={styles.box}>
|
||||
<span className={styles.boxTitleOptional}>image/video/audio</span>
|
||||
<div className={styles.boxContent}>
|
||||
{isUploading ? (
|
||||
<div className={styles.uploading}>
|
||||
<LoadingEllipsis string={t('uploading')} />
|
||||
</div>
|
||||
) : (
|
||||
<div {...getRootProps()} className={`${styles.uploadBox} ${isDragActive ? styles.dragging : ''}`}>
|
||||
<input {...getInputProps()} />
|
||||
<div className={styles.cameraIcon} />
|
||||
<div className={styles.dropText}>Drop here or</div>
|
||||
<label onClick={() => (isUploading || isChoosingFile ? null : handleUpload())}>
|
||||
<div className={styles.fileUploadIcon} />
|
||||
{t('choose_file')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.box}>
|
||||
<span className={styles.boxTitleRequired}>{t('title')}</span>
|
||||
<div className={styles.boxContent}>
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@@ -3753,6 +3753,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.3.tgz#94e247168a18342477639126da8f01353437e8d0"
|
||||
integrity sha512-QvlqvYtGBYIDeO8dFdY4djkRubcrc+yTJtBc7n8VZPlJDUS/00A+PssbvERM8f9bYRmcaSEHPZgZojeQj7kzAA==
|
||||
|
||||
"@types/node-fetch@2":
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03"
|
||||
integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.7.7":
|
||||
version "22.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4"
|
||||
@@ -4557,6 +4565,11 @@ atob@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
attr-accept@^2.2.4:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e"
|
||||
integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==
|
||||
|
||||
available-typed-arrays@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
||||
@@ -7388,6 +7401,13 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-selector@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
|
||||
integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==
|
||||
dependencies:
|
||||
tslib "^2.7.0"
|
||||
|
||||
file-type@16.5.4:
|
||||
version "16.5.4"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
|
||||
@@ -7519,7 +7539,7 @@ foreground-child@^3.1.0:
|
||||
cross-spawn "^7.0.6"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
form-data@^4.0.0, form-data@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
|
||||
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
||||
@@ -10714,7 +10734,7 @@ node-domexception@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-2.0.1.tgz#83b0d101123b5bbf91018fd569a58b88ae985e5b"
|
||||
integrity sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w==
|
||||
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.7, node-fetch@^2.7.0:
|
||||
node-fetch@2, node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.7, node-fetch@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
@@ -11710,6 +11730,15 @@ react-dom@19.0.0:
|
||||
dependencies:
|
||||
scheduler "^0.25.0"
|
||||
|
||||
react-dropzone@^14.3.8:
|
||||
version "14.3.8"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582"
|
||||
integrity sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==
|
||||
dependencies:
|
||||
attr-accept "^2.2.4"
|
||||
file-selector "^2.1.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-i18next@13.2.2:
|
||||
version "13.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.2.2.tgz#b1e78ed66a54f4bc819616f68b98221e1b1a1936"
|
||||
@@ -13354,7 +13383,7 @@ tslib@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1:
|
||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0, tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
Reference in New Issue
Block a user