upd: view file

Deskripsi:
- view file pada pengumuman, diskusi divisi dan diskusi umum

No Issues
This commit is contained in:
2026-02-04 11:37:57 +08:00
parent 9bab420f91
commit bbacd40ae9
3 changed files with 174 additions and 90 deletions

View File

@@ -14,7 +14,7 @@ import { startActivityAsync } from 'expo-intent-launcher';
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import * as Sharing from 'expo-sharing'; import * as Sharing from 'expo-sharing';
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Alert, Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"; import { Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import ImageViewing from 'react-native-image-viewing'; import ImageViewing from 'react-native-image-viewing';
import * as mime from 'react-native-mime-types'; import * as mime from 'react-native-mime-types';
import RenderHTML from 'react-native-render-html'; import RenderHTML from 'react-native-render-html';
@@ -68,6 +68,7 @@ export default function DetailAnnouncement() {
* Opens the image preview modal for the selected image file * Opens the image preview modal for the selected image file
* @param item The file data object containing image information * @param item The file data object containing image information
*/ */
function handleChooseFile(item: FileData) { function handleChooseFile(item: FileData) {
setChooseFile(item) setChooseFile(item)
setPreview(true) setPreview(true)
@@ -156,11 +157,13 @@ export default function DetailAnnouncement() {
} }
} catch (openError) { } catch (openError) {
console.error('Error opening file:', openError); console.error('Error opening file:', openError);
Alert.alert('INFO', 'Tidak ada aplikasi yang dapat membuka file ini'); Toast.show({
type: 'error',
text1: 'Tidak ada aplikasi yang dapat membuka file ini'
});
} }
} catch (error) { } catch (error) {
console.error('Error downloading or opening file:', error); console.error('Error downloading or opening file:', error);
Alert.alert('INFO', 'Gagal mengunduh atau membuka file');
Toast.show({ Toast.show({
type: 'error', type: 'error',
text1: 'Gagal membuka file', text1: 'Gagal membuka file',

View File

@@ -80,8 +80,6 @@ export default function DetailDiscussionGeneral() {
}) })
const [viewEdit, setViewEdit] = useState(false) const [viewEdit, setViewEdit] = useState(false)
useEffect(() => { useEffect(() => {
const onValueChange = reference.on('value', snapshot => { const onValueChange = reference.on('value', snapshot => {
if (snapshot.val() == null) { if (snapshot.val() == null) {

View File

@@ -1,14 +1,17 @@
import { ColorsStatus } from "@/constants/ColorsStatus"; import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv"; import { ConstEnv } from "@/constants/ConstEnv";
import { isImageFile } from "@/constants/FileExtensions";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { Ionicons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
import { startActivityAsync } from 'expo-intent-launcher'; import { startActivityAsync } from 'expo-intent-launcher';
import * as Sharing from 'expo-sharing'; import * as Sharing from 'expo-sharing';
import React, { useState } from "react"; import React, { useState } from "react";
import { Alert, Dimensions, Platform, Pressable, View } from "react-native"; import { Dimensions, Platform, Pressable, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView } from "react-native-gesture-handler";
import ImageViewing from "react-native-image-viewing";
import * as mime from 'react-native-mime-types'; import * as mime from 'react-native-mime-types';
import Toast from "react-native-toast-message";
import Text from "./Text"; import Text from "./Text";
@@ -33,26 +36,47 @@ type Props = {
dataFile: { id: string; idStorage: string; name: string; extension: string }[] dataFile: { id: string; idStorage: string; name: string; extension: string }[]
} }
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll, dataFile }: Props) { export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll, dataFile }: Props) {
const lebarDim = Dimensions.get("window").width; const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto'; const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black'; const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false); const [isTap, setIsTap] = useState(false);
const [loadingOpen, setLoadingOpen] = useState(false) const [loadingOpen, setLoadingOpen] = useState(false)
const [chooseFile, setChooseFile] = useState<PropsFile>()
const [preview, setPreview] = useState(false)
function handleChooseFile(item: PropsFile) {
setChooseFile(item)
setPreview(true)
}
const openFile = (item: { idStorage: string; name: string; extension: string }) => { const openFile = async (item: PropsFile) => {
if (Platform.OS == 'android') setLoadingOpen(true) try {
let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage; setLoadingOpen(true);
const fileName = item.name + '.' + item.extension; const remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
let localPath = `${FileSystem.documentDirectory}/${fileName}`; const fileName = item.name + '.' + item.extension;
const mimeType = mime.lookup(fileName) const localPath = `${FileSystem.documentDirectory}/${fileName}`;
const mimeType = mime.lookup(fileName);
// Download the file
const downloadResult = await FileSystem.downloadAsync(remoteUrl, localPath);
if (downloadResult.status !== 200) {
throw new Error(`Download failed with status ${downloadResult.status}`);
}
const contentURL = await FileSystem.getContentUriAsync(downloadResult.uri);
FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => {
const contentURL = await FileSystem.getContentUriAsync(uri);
setLoadingOpen(false)
try { try {
if (Platform.OS == 'android') { if (Platform.OS === 'android') {
await startActivityAsync( await startActivityAsync(
'android.intent.action.VIEW', 'android.intent.action.VIEW',
{ {
@@ -61,89 +85,148 @@ export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress
type: mimeType as string, type: mimeType as string,
} }
); );
} else if (Platform.OS == 'ios') { } else if (Platform.OS === 'ios') {
Sharing.shareAsync(localPath); await Sharing.shareAsync(localPath);
} }
} catch (error) { } catch (openError) {
Alert.alert('INFO', 'Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini'); console.error('Error opening file:', openError);
} finally { Toast.show({
if (Platform.OS == 'android') setLoadingOpen(false) type: 'error',
text1: 'Tidak ada aplikasi yang dapat membuka file ini'
});
} }
}); } catch (error) {
console.error('Error downloading or opening file:', error);
Toast.show({
type: 'error',
text1: 'Gagal membuka file',
text2: 'Silakan coba lagi nanti'
});
} finally {
setLoadingOpen(false);
}
}; };
return ( return (
<Pressable onLongPress={onLongPress} onPress={onPress} <>
onPressIn={() => setIsTap(true)} <Pressable onLongPress={onLongPress} onPress={onPress}
onPressOut={() => setIsTap(false)} onPressIn={() => setIsTap(true)}
style={({ pressed }) => [ onPressOut={() => setIsTap(false)}
borderType == 'bottom' style={({ pressed }) => [
? Styles.wrapItemBorderBottom borderType == 'bottom'
: borderType == 'all' ? Styles.wrapItemBorderBottom
? Styles.wrapItemBorderAll : borderType == 'all'
: Styles.wrapItemBorderNone, ? Styles.wrapItemBorderAll
bgColor && bgColor == 'white' && ColorsStatus.white, : Styles.wrapItemBorderNone,
// efek warna saat ditekan (sementara) bgColor && bgColor == 'white' && ColorsStatus.white,
isTap && colorPress && ColorsStatus.pressedGray, // efek warna saat ditekan (sementara)
]} isTap && colorPress && ColorsStatus.pressedGray,
> ]}
<View style={[Styles.rowItemsCenter]}> >
{icon} <View style={[Styles.rowItemsCenter]}>
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}> {icon}
<View style={[Styles.ml10, rightTopInfo ? { width: '70%' } : { width: '90%' }]}> <View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text> <View style={[Styles.ml10, rightTopInfo ? { width: '70%' } : { width: '90%' }]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
{
subtitle &&
typeof subtitle == "string"
? <Text style={[Styles.textMediumNormal, { lineHeight: 15, color: textColorFix }]}>{subtitle}</Text>
: <View style={{ alignItems: 'flex-start' }}>
{subtitle}
</View>
}
</View>
{ {
subtitle && rightTopInfo && <Text style={[Styles.textInformation, Styles.mt05, { color: textColorFix }]}>{rightTopInfo}</Text>
typeof subtitle == "string"
? <Text style={[Styles.textMediumNormal, { lineHeight: 15, color: textColorFix }]}>{subtitle}</Text>
: <View style={{ alignItems: 'flex-start' }}>
{subtitle}
</View>
} }
</View> </View>
{
rightTopInfo && <Text style={[Styles.textInformation, Styles.mt05, { color: textColorFix }]}>{rightTopInfo}</Text>
}
</View>
</View> </View>
{desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'left', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>} {desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'left', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>}
{ {
dataFile.length > 0 && ( dataFile.length > 0 && (
<ScrollView horizontal style={[Styles.mv05]} showsHorizontalScrollIndicator={false}> <ScrollView horizontal style={[Styles.mv05]} showsHorizontalScrollIndicator={false}>
{dataFile.map((item, index) => ( {dataFile.map((item, index) => (
<Pressable <Pressable
key={index} key={index}
style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]} style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}
onPress={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }} onPress={() => {
> isImageFile(item.extension) ?
<Ionicons name="document-text" size={18} color="grey" style={Styles.mr05} /> handleChooseFile(item)
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{item.name}.{item.extension}</Text> : openFile(item)
</Pressable> }}
))} >
</ScrollView> <MaterialCommunityIcons
) name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
} size={18}
{ color="grey" />
(leftBottomInfo || rightBottomInfo) && <Text style={[Styles.textInformation, Styles.cGray]}>{item.name}.{item.extension}</Text>
( </Pressable>
<View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}> ))}
{ </ScrollView>
typeof leftBottomInfo == 'string' ? )
<Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text> }
: {
leftBottomInfo (leftBottomInfo || rightBottomInfo) &&
} (
{ <View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}>
typeof rightBottomInfo == 'string' ? {
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text> typeof leftBottomInfo == 'string' ?
: <Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text>
rightBottomInfo :
} leftBottomInfo
}
{
typeof rightBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text>
:
rightBottomInfo
}
</View>
)
}
</Pressable>
<ImageViewing
images={[{ uri: `${ConstEnv.url_storage}/files/${chooseFile?.idStorage}` }]}
imageIndex={0}
visible={preview}
onRequestClose={() => setPreview(false)}
doubleTapToZoomEnabled
HeaderComponent={({ imageIndex }) => (
<View style={[Styles.headerModalViewImg]}>
{/* CLOSE */}
<Pressable
onPress={() => setPreview(false)}
accessibilityRole="button"
accessibilityLabel="Close image viewer"
>
<Text style={{ color: 'white', fontSize: 26 }}></Text>
</Pressable>
{/* MENU */}
<Pressable
onPress={() => chooseFile && openFile(chooseFile)}
accessibilityRole="button"
accessibilityLabel="Download or share image"
disabled={loadingOpen}
>
<Text style={{ color: loadingOpen ? 'gray' : 'white', fontSize: 22 }}></Text>
</Pressable>
</View> </View>
) )}
} FooterComponent={({ imageIndex }) => (
</Pressable> <View style={{
paddingBottom: 20,
paddingHorizontal: 16,
alignItems: 'center',
}}>
<Text style={{ color: 'white', fontSize: 16 }}>{chooseFile?.name}.{chooseFile?.extension}</Text>
</View>
)}
/>
</>
) )
} }