Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ android/app/release/
node_modules/
npm-debug.log
yarn-error.log
# package-lock.json
package-lock.json

android/app/google-services.json
android/keystore.properties
Expand Down
45 changes: 42 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, {useEffect} from 'react';
// import 'react-native-reanimated';

import {StatusBar, StyleSheet} from 'react-native';
import {
StatusBar,
StyleSheet,
PermissionsAndroid,
Platform,
} from 'react-native';
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
import Toast from 'react-native-toast-message';
import MainApp from './src/main';
Expand All @@ -14,8 +17,43 @@ enableScreens();
function App(): React.JSX.Element {
useEffect(() => {
SplashScreen.hide();
requestStoragePermission();
}, []);

const requestStoragePermission = async () => {
try {
if (Platform.OS === 'android') {
if (Platform.Version >= 33) {
// Android 13+ (Images, Video, Audio separately)
const result = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
PermissionsAndroid.PERMISSIONS.READ_MEDIA_AUDIO,
]);
console.log('Android 13+ permission result:', result);
} else {
// Android 12 and below
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
{
title: 'Storage Permission Required',
message:
'ScheduleX needs access to your storage to function properly.',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage permission granted');
} else {
console.log('Storage permission denied');
}
}
}
} catch (err) {
console.warn(err);
}
};

return (
<SafeAreaProvider>
<SafeAreaView style={styles.safeArea} edges={['top']}>
Expand Down Expand Up @@ -44,4 +82,5 @@ const styles = StyleSheet.create({
backgroundColor: '#18181B',
},
});

export default App;
64 changes: 39 additions & 25 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.schedulex">

<!-- Internet access -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- For Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<!-- For Android 10 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />

<!-- Optional: only if your app is a file manager or needs all-file access -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">

<activity
android:name=".MainActivity"
android:name=".MainApplication"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:screenOrientation="portrait"
>

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">

</manifest>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>
</manifest>
Empty file.
49 changes: 30 additions & 19 deletions src/utils/exportSchedule.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
/**
* Rename an exported CSV file in the Downloads directory.
* @param oldName The current filename (with .csv)
* @param newName The new filename (without .csv or with .csv)
* @returns Promise<boolean> true if successful, false otherwise
*/
export async function renameExportedCSV(oldName: string, newName: string): Promise<boolean> {
try {
const downloadsDir = RNFS.DownloadDirectoryPath;
const oldPath = `${downloadsDir}/${oldName}`;
let newFileName = newName.endsWith('.csv') ? newName : `${newName}.csv`;
const newPath = `${downloadsDir}/${newFileName}`;
const exists = await RNFS.exists(oldPath);
if (!exists) {
Alert.alert('Rename Failed', `File ${oldName} does not exist.`);
return false;
}
await RNFS.moveFile(oldPath, newPath);
ToastAndroid.show(`Renamed to ${newFileName}`, ToastAndroid.SHORT);
return true;
} catch (e) {
console.error('Rename error:', e);
Alert.alert('Rename Failed', `Could not rename file: ${e instanceof Error ? e.message : 'Unknown error'}`);
return false;
}
}
import RNFS from 'react-native-fs';
import { Alert, ToastAndroid, Platform, PermissionsAndroid } from 'react-native';
import Share from 'react-native-share';
import { generateRegisterCSV } from './csv-export';
import { CardInterface } from '../types/cards';
import { PermissionsHelper } from './permissions';

export interface ExportResult {
path: string;
Expand All @@ -25,25 +52,9 @@ export class ExportScheduleUtility {
}

private async checkStoragePermission(): Promise<boolean> {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
{
title: 'Storage Permission',
message: 'App needs access to storage to save CSV files',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn(err);
return false;
}
}
return true; // iOS doesn't need explicit storage permission for app documents
// Use PermissionsHelper for consistent permission logic
const result = await PermissionsHelper.requestStoragePermission();
return result.granted;
}

private getAllRegisterIds(): number[] {
Expand Down
6 changes: 6 additions & 0 deletions src/utils/storage-permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PermissionsHelper } from './permissions';

export const requestStoragePermission = async (): Promise<boolean> => {
const result = await PermissionsHelper.requestStoragePermission();
return result.granted;
};