Skip to content

Commit

Permalink
Paste images from your clipboard in android (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
krrish-sehgal authored Oct 30, 2024
1 parent 6e1f072 commit df46f31
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 34 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/build-flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '17'
distribution: "zulu"
java-version: "17"
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
architecture: x64
- run: flutter pub get
- run: flutter analyze
- run: flutter test
- run: flutter build apk

# Upload APK as a build artifact
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: app-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
Expand All @@ -35,7 +35,7 @@ jobs:
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
architecture: x64

- name: Clean the build environment
Expand All @@ -61,7 +61,7 @@ jobs:
- name: Build macOS application with verbose logging
run: flutter build macos --verbose

- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: macos-release-app
path: build/macos/Build/Products/Release/*.app
Expand All @@ -75,7 +75,7 @@ jobs:
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
architecture: x64

- name: Clean the build environment
Expand All @@ -92,7 +92,7 @@ jobs:
- name: Build Linux application with verbose logging
run: flutter build linux --verbose

- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: linux-release-app
path: build/linux/x64/release/bundle/*
Expand All @@ -106,7 +106,7 @@ jobs:
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
architecture: x64

- name: Clean the build environment
Expand All @@ -125,7 +125,7 @@ jobs:
- name: Build Windows application with verbose logging
run: flutter build windows --verbose

- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: windows-release-app
path: build/windows/x64/runner/Release/*.exe
5 changes: 5 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,9 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.multidex:multidex:2.0.1"

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.activity:activity-ktx:1.7.2'

}
44 changes: 43 additions & 1 deletion android/app/src/main/kotlin/com/apps/blt/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
package com.apps.blt

import android.content.ClipData
import android.content.ClipboardManager
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.provider.MediaStore
import android.util.Base64
import androidx.annotation.NonNull
import androidx.annotation.RequiresApi
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream

class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity() {
private val CHANNEL = "clipboard_image_channel"

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getClipboardImage") {
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = clipboard.primaryClip

if (clipData != null && clipData.itemCount > 0) {
val item = clipData.getItemAt(0)

if (item.uri != null) {
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, item.uri)
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
val byteArray = stream.toByteArray()
val base64String = Base64.encodeToString(byteArray, Base64.DEFAULT)
result.success(base64String)
} else {
result.error("NO_IMAGE", "Clipboard does not contain an image", null)
}
} else {
result.error("EMPTY_CLIPBOARD", "Clipboard is empty", null)
}
} else {
result.notImplemented()
}
}
}
}
7 changes: 4 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
jcenter()
mavenCentral()

}

dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
4 changes: 2 additions & 2 deletions android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
#Tue Oct 29 01:30:50 IST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
103 changes: 87 additions & 16 deletions lib/src/pages/home/report_bug.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:blt/src/pages/home/home_imports.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/services.dart'; // For method channel
import 'dart:convert';
import 'package:path_provider/path_provider.dart';

import '../../models/tags_model.dart';
import '../../util/api/tags_api.dart';
Expand Down Expand Up @@ -73,6 +76,25 @@ class _ReportFormState extends ConsumerState<ReportForm> {
bool showLabel = false;
List<Tag> _labels = [];
late List<bool> _labelsState;
static const platform = MethodChannel('clipboard_image_channel');
bool _isSnackBarVisible = false;

void showSnackBar(BuildContext context, String message) {
if (!_isSnackBarVisible) {
_isSnackBarVisible = true;
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(message),
duration: Duration(seconds: 3),
),
)
.closed
.then((_) {
_isSnackBarVisible = false;
});
}
}

Future<void> _pickImageFromGallery() async {
final imageFile = await picker.pickMultiImage();
Expand All @@ -88,6 +110,30 @@ class _ReportFormState extends ConsumerState<ReportForm> {
}
}

Future<void> _pasteImage() async {
try {
String base64Image = await platform.invokeMethod('getClipboardImage');
base64Image = base64Image.replaceAll(RegExp(r'\s+'), '');
while (base64Image.length % 4 != 0) {
base64Image += "=";
}
Uint8List decodedImage = base64Decode(base64Image);
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;

File tempFile = File(
'$tempPath/temp_image_${DateTime.now().millisecondsSinceEpoch}.png');

await tempFile.writeAsBytes(decodedImage);

setState(() {
_image.add(tempFile);
});
} on PlatformException {
showSnackBar(context, 'No image available on clipboard');
}
}

// Future<File> _coverToImage(Uint8List imageBytes) async {
// String tempPath = (await getTemporaryDirectory()).path;
// File file = File('$tempPath/profile.png');
Expand All @@ -96,21 +142,6 @@ class _ReportFormState extends ConsumerState<ReportForm> {
// return file;
// }

// Future<void> _pasteImageFromClipBoard() async {
// try {
// final imageBytes = await Pasteboard.image;
// late File? image;
// if (imageBytes != null) {
// image = await _coverToImage(imageBytes);
// }
// setState(() {
// _image = image;
// });
// } catch (e) {
// print('No Image Found On Clipboard');
// }
// }

void markdownFormatting(String formatter) {
int start = _descriptionController.selection.baseOffset;
int end = _descriptionController.selection.extentOffset;
Expand Down Expand Up @@ -818,7 +849,7 @@ class _ReportFormState extends ConsumerState<ReportForm> {
height: 125.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: min(5, _image.length + 1),
itemCount: _image.length < 5 ? _image.length + 2 : 5,
itemBuilder: (_, i) {
if (i < _image.length) {
return Container(
Expand Down Expand Up @@ -874,6 +905,46 @@ class _ReportFormState extends ConsumerState<ReportForm> {
),
),
);
} else if (i == _image.length) {
return SizedBox(
width: 125.0,
child: Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: _pasteImage,
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Color(0xFFF8D2CD),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.paste,
color: Color(0xFFDC4654),
size: 35.0,
),
SizedBox(height: 10),
Text(
"Paste image",
style: GoogleFonts.ubuntu(
textStyle: TextStyle(
color: Color(0xFFDC4654),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
);
} else {
return SizedBox(
width: 125.0,
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies:
shared_preferences: ^2.0.5
flutter_svg: ">=1.0.3 <3.0.0"
flutter_riverpod: ^1.0.3
intl:
intl:
flutter_secure_storage: ^9.0.0
smooth_page_indicator: ^1.0.0+2
pasteboard: ^0.2.0
Expand Down

0 comments on commit df46f31

Please sign in to comment.