Skip to content

Commit

Permalink
Merge pull request #110 from iiumschedule/fix/gallery-save
Browse files Browse the repository at this point in the history
Fix saving schedule to Gallery [Draft]
  • Loading branch information
iqfareez authored Mar 23, 2024
2 parents bb770a7 + 01b121d commit f001b00
Show file tree
Hide file tree
Showing 22 changed files with 229 additions and 141 deletions.
9 changes: 7 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- Permissions options for the `storage` group -->
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<!-- Gal: For supporting API <= 29 :Gal-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<!-- Gal: For supporting API <= 29 :Gal-->

<!-- Required by device_calendar -->
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
Expand All @@ -18,7 +22,8 @@
</queries>
<application
android:icon="@mipmap/ic_launcher"
android:label="IIUM Schedule">
android:label="IIUM Schedule"
android:requestLegacyExternalStorage="true"> <!-- Gal: For saving to album in API 29 :Gal-->
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
Expand Down
2 changes: 1 addition & 1 deletion ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
33 changes: 20 additions & 13 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ PODS:
- fluttertoast (0.0.2):
- Flutter
- Toast
- gal (1.0.0):
- Flutter
- FlutterMacOS
- isar_flutter_libs (1.0.0):
- Flutter
- open_file (0.0.1):
Expand All @@ -22,7 +25,7 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.1.1):
- permission_handler_apple (9.3.0):
- Flutter
- quick_actions_ios (0.0.1):
- Flutter
Expand All @@ -37,6 +40,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- open_file (from `.symlinks/plugins/open_file/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
Expand All @@ -60,6 +64,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
open_file:
Expand All @@ -78,21 +84,22 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"

SPEC CHECKSUMS:
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
quick_actions_ios: 9e80dcfadfbc5d47d9cf8f47bcf428b11cf383d4
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
quick_actions_ios: d24571db7345d2e48d094db8d077a015a568002d
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586

PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796

COCOAPODS: 1.12.1
COCOAPODS: 1.15.2
8 changes: 4 additions & 4 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
Expand Down Expand Up @@ -452,7 +452,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -579,7 +579,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -628,7 +628,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
8 changes: 8 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,13 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- gal: Required -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save images to device's gallery</string>
<!-- gal: Required -->
<!-- gal: Required for ios < 14 or saving to album -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to save images to device's gallery</string>
<!-- gal: Required for ios < 14 or saving to album -->
</dict>
</plist>
4 changes: 1 addition & 3 deletions lib/constants.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import 'dart:io';

import 'package:flutter/foundation.dart';

/// default session/current academic seesion
const String kDefaultSession = '2023/2024';

/// Values must be between 1 and 3 (inclusive)
const int kDefaultSemester = 2;

/// Check if app is running on macos or iphones/ipads
final kIsApple = !kIsWeb && (Platform.isMacOS || Platform.isIOS);
final kIsApple = Platform.isMacOS || Platform.isIOS;
92 changes: 56 additions & 36 deletions lib/features/schedule_viewer/saved/utils/save_file.dart
Original file line number Diff line number Diff line change
@@ -1,48 +1,68 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:gal/gal.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

class SaveFile {
Future<String?> save(
Uint8List pngBytes, String filename, bool tempPath) async {
String? saveDirectory;
if (tempPath) {
if (Platform.isWindows) {
saveDirectory = await getApplicationDocumentsDirectory()
.then((value) => value.path);
} else {
// Android
saveDirectory =
await getTemporaryDirectory().then((value) => value.path);
}
} else {
if (Platform.isAndroid) {
var status = await Permission.storage.request();
if (status.isDenied) throw Exception('Permission denied');

// create/use Picture folder

final folderDirectory =
await Directory('/storage/emulated/0/Pictures/IIUM Schedule')
.create();
saveDirectory = folderDirectory.path;
}

if (Platform.isWindows) {
final downloadDirectory = await getDownloadsDirectory();
// use download folder
final folderDirectory =
await Directory('${downloadDirectory!.path}/IIUM Schedule')
.create();

saveDirectory = folderDirectory.path;
}
/// Save image to device gallery. Permission will be asked
/// Returns the path of the saved file (if available, null is not indicator of error)
static Future<void> saveToGallery(Uint8List pngBytes, String filename) async {
// sanitize file name and change space to dash
final filenameSanitized =
filename.replaceAll(RegExp(r'[^\w\s]+'), '').replaceAll(' ', '-');
String saveDirectory =
await getTemporaryDirectory().then((value) => value.path);

File imgFile = File('$saveDirectory/$filenameSanitized.png');
await imgFile.writeAsBytes(pngBytes);

// Request access permission.
// On Android, permission is needed for Android >=23 && <=29 only. Else, it will
// be allowed by default.
// There is an issue with MacOs don't show permission dialog when app from IDE feature
// https://github.com/natsuk4ze/gal?tab=readme-ov-file#macos
// https://github.com/flutter/flutter/issues/134191
await Gal.requestAccess(toAlbum: true);

// the maintainer said using putImage is better than putImageBytes
// https://github.com/natsuk4ze/gal/wiki/Best-Practice
try {
await Gal.putImage(imgFile.path, album: 'IIUM Schedule');
} on GalException catch (e) {
throw e.type.message;
}
}

/// Save image to Downloads folder
/// Returns the path of the saved file
static Future<String?> saveToDownloads(
Uint8List pngBytes, String filename) async {
String? saveDirectory;

final downloadDirectory = await getDownloadsDirectory();
final folderDirectory =
await Directory('${downloadDirectory!.path}/IIUM Schedule').create();

saveDirectory = folderDirectory.path;

File imgFile = File('$saveDirectory/$filename.png');
await imgFile.writeAsBytes(pngBytes);

return imgFile.path;
}

/// Save file in temporary directory
static Future<String?> saveTemp(Uint8List pngBytes) async {
String saveDirectory =
await getTemporaryDirectory().then((value) => value.path);

// generate random filename
String filename = DateTime.now().millisecondsSinceEpoch.toString();

// Save bytes to file
File imgFile = File('$saveDirectory/$filename.png');
imgFile.writeAsBytes(pngBytes);
await imgFile.writeAsBytes(pngBytes);

return imgFile.path;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ScheduleShare {
// To make the schedule title as caption, set either [subject] or [text]
// On Android, set [text]
// On Ios, set [subject] (when set text, the share sheet display two files)
Share.shareXFiles(
await Share.shareXFiles(
[xFilePath],
subject: !Platform.isAndroid ? scheduleTitle : null,
text: !Platform.isIOS ? scheduleTitle : null,
Expand Down
18 changes: 4 additions & 14 deletions lib/features/schedule_viewer/saved/utils/screenshot_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import 'save_file.dart';

class ScreenshotWidget {
/// Capture the screenshot of the given widget wrapped in RepaintBoundary
/// and return the path to the saved image
static Future<String?> screenshotAndSave(
GlobalKey<State<StatefulWidget>> globalKey, String name,
{bool tempPath = false}) async {
SaveFile sf = SaveFile();
/// and return the image bytes
static Future<Uint8List?> screenshotWidget(
GlobalKey<State<StatefulWidget>> globalKey) async {
RenderRepaintBoundary boundary =
globalKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
// In debug mode Android sometimes will return !debugNeedsPrint error
Expand All @@ -22,15 +18,9 @@ class ScreenshotWidget {
await Future.delayed(const Duration(milliseconds: 20));
}
ui.Image image = await boundary.toImage(pixelRatio: 2.5);
// TODO: Saves to gallery

ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
// sanitize file name and change space to dash
final filename =
name.replaceAll(RegExp(r'[^\w\s]+'), '').replaceAll(' ', '-');

// return saved path of the image, if web it will return null
return await sf.save(pngBytes, filename, tempPath);
return pngBytes;
}
}
Loading

0 comments on commit f001b00

Please sign in to comment.