mirror of
https://github.com/Stef-00012/Zipline-Android-App.git
synced 2025-05-10 18:05:52 +02:00
755 lines
19 KiB
TypeScript
755 lines
19 KiB
TypeScript
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
|
import { type ExternalPathString, useRouter } from "expo-router";
|
|
import { styles } from "@/styles/components/largeFileDisplay";
|
|
import FileDisplay from "@/components/FileDisplay";
|
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
import { getTags } from "@/functions/zipline/tags";
|
|
import { convertToBytes } from "@/functions/util";
|
|
import { isLightColor } from "@/functions/color";
|
|
import * as FileSystem from "expo-file-system";
|
|
import TextInput from "@/components/TextInput";
|
|
import { useEffect, useState } from "react";
|
|
import * as Clipboard from "expo-clipboard";
|
|
import * as db from "@/functions/database";
|
|
import Select from "@/components/Select";
|
|
import Button from "@/components/Button";
|
|
import Popup from "@/components/Popup";
|
|
import React from "react";
|
|
import axios from "axios";
|
|
import {
|
|
removeFileFromFolder,
|
|
addFileToFolder,
|
|
getFolders,
|
|
} from "@/functions/zipline/folders";
|
|
import {
|
|
type EditFileOptions,
|
|
deleteFile,
|
|
editFile,
|
|
} from "@/functions/zipline/files";
|
|
import type {
|
|
APIFoldersNoIncl,
|
|
APIFile,
|
|
APITags,
|
|
DashURL,
|
|
} from "@/types/zipline";
|
|
import {
|
|
type ColorValue,
|
|
ToastAndroid,
|
|
Pressable,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
|
|
interface Props {
|
|
file: APIFile;
|
|
hidden: boolean;
|
|
onClose: (refresh?: boolean) => void | Promise<void>;
|
|
}
|
|
|
|
export default function LargeFileDisplay({ file, hidden, onClose }: Props) {
|
|
const router = useRouter();
|
|
|
|
const dashUrl = db.get("url") as DashURL | null;
|
|
|
|
const [tags, setTags] = useState<APITags>([]);
|
|
const [folders, setFolders] = useState<APIFoldersNoIncl>([]);
|
|
|
|
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
|
|
const [filePassword, setFilePassword] = useState<boolean>(file.password);
|
|
const [fileMaxViews, setFileMaxViews] = useState<number | null>(
|
|
file.maxViews,
|
|
);
|
|
const [fileOriginalName, setFileOriginalName] = useState<string | null>(
|
|
file.originalName,
|
|
);
|
|
const [fileType, setFileType] = useState<string>(file.type);
|
|
const [fileFolderId, setFileFolderId] = useState<string | null>(
|
|
file.folderId,
|
|
);
|
|
const [fileFavorite, setFileFavorite] = useState<boolean>(file.favorite);
|
|
|
|
const [tempHidden, setTempHidden] = useState<boolean>(false);
|
|
|
|
const [deleteFilePopup, setDeleteFilePopup] = useState<boolean>(false);
|
|
const [editFilePopup, setEditFilePopup] = useState<boolean>(false);
|
|
|
|
const [editFileMaxViews, setEditFileMaxViews] = useState<number | null>(
|
|
file.maxViews,
|
|
);
|
|
const [editFileOriginalName, setEditFileOriginalName] = useState<
|
|
string | null
|
|
>(file.originalName);
|
|
const [editFileType, setEditFileType] = useState<string>(file.type);
|
|
const [editFilePassword, setEditFilePassword] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const tags = await getTags();
|
|
const folders = await getFolders(true);
|
|
|
|
setTags(typeof tags === "string" ? [] : tags);
|
|
setFolders(typeof folders === "string" ? [] : folders);
|
|
})();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (fileType.startsWith("text/")) {
|
|
(async () => {
|
|
const res = await axios.get(`${dashUrl}/raw/${file.name}`, {
|
|
responseType: "text",
|
|
});
|
|
|
|
setFileContent(res.data as string);
|
|
})();
|
|
}
|
|
|
|
setDeleteFilePopup(false);
|
|
setTempHidden(false);
|
|
}, [file, dashUrl, fileType]);
|
|
|
|
return (
|
|
<>
|
|
<Popup
|
|
hidden={!deleteFilePopup}
|
|
onClose={() => {
|
|
setDeleteFilePopup(false);
|
|
setTempHidden(false);
|
|
}}
|
|
>
|
|
<View style={styles.popupContent}>
|
|
<Text style={styles.mainHeaderText}>Are you sure?</Text>
|
|
|
|
<Text style={styles.serverActionWarningText}>
|
|
Are you sure you want to delete {file.name}? This action cannot be
|
|
undone.
|
|
</Text>
|
|
|
|
<View style={styles.fileDeleteButtonsContainer}>
|
|
<Button
|
|
color="#181c28"
|
|
text="Cancel"
|
|
onPress={() => {
|
|
setDeleteFilePopup(false);
|
|
setTempHidden(false);
|
|
}}
|
|
margin={{
|
|
top: 10,
|
|
right: 10,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
color="#CF4238"
|
|
text={`Delete ${file.name}`}
|
|
onPress={async () => {
|
|
const fileId = file.id;
|
|
|
|
const success = await deleteFile(fileId);
|
|
|
|
if (typeof success === "string") {
|
|
ToastAndroid.show(`Error: ${success}`, ToastAndroid.SHORT);
|
|
|
|
setDeleteFilePopup(false);
|
|
setTempHidden(false);
|
|
|
|
return;
|
|
}
|
|
|
|
setDeleteFilePopup(false);
|
|
setTempHidden(false);
|
|
onClose(true);
|
|
|
|
ToastAndroid.show(
|
|
`Successfully deleted the file ${file.name}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
margin={{
|
|
top: 10,
|
|
right: 10,
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.popupSubHeaderText}>
|
|
Press outside to close this popup
|
|
</Text>
|
|
</Popup>
|
|
|
|
<Popup
|
|
hidden={!editFilePopup}
|
|
onClose={() => {
|
|
setEditFilePopup(false);
|
|
setTempHidden(false);
|
|
}}
|
|
>
|
|
<View style={styles.popupContent}>
|
|
<Text style={styles.mainHeaderText}>Editing "{file.name}"</Text>
|
|
|
|
<TextInput
|
|
title="Max Views:"
|
|
onValueChange={(content) => {
|
|
setEditFileMaxViews(Math.abs(Number.parseInt(content)));
|
|
}}
|
|
value={editFileMaxViews ? String(editFileMaxViews) : ""}
|
|
keyboardType="numeric"
|
|
placeholder="Unlimited"
|
|
/>
|
|
|
|
<TextInput
|
|
title="Original Name:"
|
|
onValueChange={(content) => {
|
|
setEditFileOriginalName(content);
|
|
}}
|
|
value={editFileOriginalName || ""}
|
|
/>
|
|
|
|
<TextInput
|
|
title="Type:"
|
|
onValueChange={(content) => {
|
|
setEditFileType(content);
|
|
}}
|
|
value={editFileType || ""}
|
|
/>
|
|
|
|
{filePassword ? (
|
|
<Button
|
|
color="#CF4238"
|
|
text="Remove Password"
|
|
onPress={() => {
|
|
const fileId = file.id;
|
|
|
|
const success = editFile(fileId, {
|
|
password: null,
|
|
});
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
setEditFilePassword(null);
|
|
|
|
setFilePassword(false);
|
|
file.password = false;
|
|
|
|
ToastAndroid.show(
|
|
"Successfully removed the password",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
margin={{
|
|
top: 10,
|
|
}}
|
|
/>
|
|
) : (
|
|
<TextInput
|
|
title="Password:"
|
|
password
|
|
onValueChange={(content) => {
|
|
setEditFilePassword(content);
|
|
}}
|
|
value={editFilePassword || ""}
|
|
/>
|
|
)}
|
|
|
|
<Button
|
|
color="#323ea8"
|
|
text="Save Changes"
|
|
icon="save"
|
|
onPress={async () => {
|
|
const fileId = file.id;
|
|
|
|
const editData: EditFileOptions = {};
|
|
|
|
editData.maxViews = editFileMaxViews || null;
|
|
editData.type = editFileType;
|
|
if (editFileOriginalName)
|
|
editData.originalName = editFileOriginalName;
|
|
if (editFilePassword) editData.password = editFilePassword;
|
|
|
|
const success = await editFile(fileId, editData);
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
if (editFilePassword) {
|
|
setFilePassword(true);
|
|
file.password = true;
|
|
}
|
|
|
|
file.originalName = editFileOriginalName || null;
|
|
setFileOriginalName(editFileOriginalName || null);
|
|
|
|
file.type = editFileType;
|
|
setFileType(editFileType);
|
|
|
|
file.maxViews = editFileMaxViews || null;
|
|
setFileMaxViews(editFileMaxViews);
|
|
|
|
setEditFilePopup(false);
|
|
setTempHidden(false);
|
|
|
|
ToastAndroid.show(
|
|
`Successfully edited the file ${file.name}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
margin={{
|
|
top: 10,
|
|
}}
|
|
/>
|
|
</View>
|
|
</Popup>
|
|
|
|
<Pressable
|
|
style={{
|
|
...styles.popupContainerOverlay,
|
|
...((hidden || tempHidden || !file) && { display: "none" }),
|
|
}}
|
|
onPress={(e) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<View style={styles.popupContainer}>
|
|
<Text style={styles.fileHeader}>{file.name}</Text>
|
|
|
|
<KeyboardAwareScrollView showsVerticalScrollIndicator={false}>
|
|
{fileContent ? (
|
|
<TextInput
|
|
multiline
|
|
showDisabledStyle={false}
|
|
disabled
|
|
inputStyle={styles.textDisplay}
|
|
value={fileContent}
|
|
/>
|
|
) : (
|
|
<FileDisplay
|
|
passwordProtected={!!filePassword}
|
|
uri={`${dashUrl}/raw/${file.name}`}
|
|
originalName={fileOriginalName}
|
|
mimetype={fileType}
|
|
name={file.name}
|
|
maxHeight={500}
|
|
width={350}
|
|
file={file}
|
|
autoHeight
|
|
/>
|
|
)}
|
|
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="description" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Type</Text>
|
|
<Text style={styles.fileInfoText}>{file.type}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="sd-storage" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Size</Text>
|
|
<Text style={styles.fileInfoText}>
|
|
{convertToBytes(file.size, {
|
|
unitSeparator: " ",
|
|
})}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="visibility" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>View</Text>
|
|
<Text style={styles.fileInfoText}>
|
|
{file.views}
|
|
{fileMaxViews &&
|
|
!Number.isNaN(fileMaxViews) &&
|
|
`/${fileMaxViews}`}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="file-upload" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Created At</Text>
|
|
<Text style={styles.fileInfoText}>
|
|
{new Date(file.createdAt).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="autorenew" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Updated At</Text>
|
|
<Text style={styles.fileInfoText}>
|
|
{new Date(file.updatedAt).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{file.deletesAt && (
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="auto-delete" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Deletes At</Text>
|
|
<Text style={styles.fileInfoText}>
|
|
{new Date(file.deletesAt).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{fileOriginalName && (
|
|
<View style={styles.fileInfoContainer}>
|
|
<MaterialIcons name="title" size={28} color="white" />
|
|
<View style={styles.fileInfoTextContainer}>
|
|
<Text style={styles.fileInfoHeader}>Original Name</Text>
|
|
<Text style={styles.fileInfoText}>{fileOriginalName}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<Text style={styles.fileInfoHeader}>Tags</Text>
|
|
<Select
|
|
placeholder="Select Tags..."
|
|
multiple
|
|
disabled={tags.length <= 0}
|
|
data={tags.map((tag) => ({
|
|
label: tag.name,
|
|
value: tag.id,
|
|
color: tag.color,
|
|
}))}
|
|
onSelect={async (selectedTags) => {
|
|
const newTags = selectedTags.map((tag) => tag.value);
|
|
|
|
const success = editFile(file.id, {
|
|
tags: newTags,
|
|
});
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
file.tags = tags.filter((tag) => newTags.includes(tag.id));
|
|
|
|
ToastAndroid.show(
|
|
"Successfully updated the tags",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
renderItem={(item) => (
|
|
<View style={styles.selectRenderItemContainer}>
|
|
<Text
|
|
style={{
|
|
...styles.selectRenderItemText,
|
|
color: isLightColor(item.color as string)
|
|
? "black"
|
|
: "white",
|
|
backgroundColor: item.color as ColorValue,
|
|
}}
|
|
>
|
|
{item.label}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
defaultValues={tags
|
|
.filter((tag) =>
|
|
file.tags.find((fileTag) => fileTag.id === tag.id),
|
|
)
|
|
.map((tag) => ({
|
|
label: tag.name,
|
|
value: tag.id,
|
|
color: tag.color,
|
|
}))}
|
|
renderSelectedItem={(item, key) => (
|
|
<Text
|
|
key={key}
|
|
style={{
|
|
...styles.selectRenderSelectedItemText,
|
|
color: isLightColor(item.color as string)
|
|
? "black"
|
|
: "white",
|
|
backgroundColor: item.color as ColorValue,
|
|
}}
|
|
>
|
|
{item.label}
|
|
</Text>
|
|
)}
|
|
maxHeight={500}
|
|
/>
|
|
|
|
<Text style={styles.fileInfoHeader}>Folder</Text>
|
|
{fileFolderId ? (
|
|
<Button
|
|
color="#e03131"
|
|
text={`Remove from folder "${folders.find((folder) => folder.id === file.folderId)?.name}"`}
|
|
onPress={async () => {
|
|
if (!fileFolderId) return;
|
|
|
|
const folderId = fileFolderId;
|
|
const fileId = file.id;
|
|
|
|
const success = removeFileFromFolder(folderId, fileId);
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
setFileFolderId(null);
|
|
file.folderId = null;
|
|
|
|
ToastAndroid.show(
|
|
"Successfully removed the file from the folder",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
margin={{
|
|
top: 5,
|
|
}}
|
|
/>
|
|
) : (
|
|
<Select
|
|
placeholder="Add to Folder..."
|
|
data={folders.map((folder) => ({
|
|
label: folder.name,
|
|
value: folder.id,
|
|
}))}
|
|
defaultValue={
|
|
file.folderId
|
|
? {
|
|
label: (
|
|
folders.find(
|
|
(folder) => folder.id === file.folderId,
|
|
) as APIFoldersNoIncl[0]
|
|
)?.name,
|
|
value: file.folderId,
|
|
}
|
|
: undefined
|
|
}
|
|
onSelect={async (selectedFolder) => {
|
|
if (selectedFolder.length <= 0) return;
|
|
|
|
const folderId = selectedFolder[0].value;
|
|
const fileId = file.id;
|
|
|
|
const success = await addFileToFolder(folderId, fileId);
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
setFileFolderId(folderId);
|
|
file.folderId = folderId;
|
|
|
|
ToastAndroid.show(
|
|
"Successfully added the file to the folder",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
maxHeight={400}
|
|
/>
|
|
)}
|
|
|
|
<Text style={styles.subHeaderText}>{file.id}</Text>
|
|
|
|
<View style={styles.actionButtonsContainer}>
|
|
<Button
|
|
icon="edit"
|
|
color="#e8590c"
|
|
onPress={() => {
|
|
setEditFilePopup(true);
|
|
setTempHidden(true);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon="delete"
|
|
color="#e03131"
|
|
onPress={() => {
|
|
setDeleteFilePopup(true);
|
|
setTempHidden(true);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon={fileFavorite ? "star" : "star-outline"}
|
|
color={fileFavorite ? "#f08c00" : "#343a40"}
|
|
onPress={async () => {
|
|
const success = editFile(file.id, {
|
|
favorite: !file.favorite,
|
|
});
|
|
|
|
if (typeof success === "string")
|
|
return ToastAndroid.show(
|
|
`Error: ${success}`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
file.favorite = !fileFavorite;
|
|
setFileFavorite((prev) => !prev);
|
|
|
|
ToastAndroid.show(
|
|
`Successfully ${fileFavorite ? "removed from" : "added to"} favorites`,
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon="open-in-new"
|
|
color="#323ea8"
|
|
onPress={() => {
|
|
router.replace(`${dashUrl}${file.url}` as ExternalPathString);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon="content-copy"
|
|
color="#343a40"
|
|
onPress={async () => {
|
|
const url = `${dashUrl}${file.url}`;
|
|
|
|
const success = await Clipboard.setStringAsync(url);
|
|
|
|
if (!success)
|
|
return ToastAndroid.show(
|
|
"Failed to copy the URL",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
ToastAndroid.show(
|
|
"Copied URL to clipboard",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
icon="file-download"
|
|
color="#343a40"
|
|
onPress={async () => {
|
|
const downloadUrl = `${dashUrl}/raw/${file.name}?download=true`;
|
|
|
|
let savedFileDownloadUri = db.get("fileDownloadPath");
|
|
|
|
if (!savedFileDownloadUri) {
|
|
const permissions =
|
|
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
|
|
|
|
if (!permissions.granted)
|
|
return ToastAndroid.show(
|
|
"The permission to save the file was not granted",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
db.set("fileDownloadPath", permissions.directoryUri);
|
|
savedFileDownloadUri = permissions.directoryUri;
|
|
}
|
|
|
|
ToastAndroid.show("Downloading...", ToastAndroid.SHORT);
|
|
|
|
const saveUri =
|
|
await FileSystem.StorageAccessFramework.createFileAsync(
|
|
savedFileDownloadUri,
|
|
file.name,
|
|
file.type,
|
|
);
|
|
|
|
const downloadResult = await FileSystem.downloadAsync(
|
|
downloadUrl,
|
|
`${FileSystem.cacheDirectory}/${file.name}`,
|
|
);
|
|
|
|
if (!downloadResult.uri)
|
|
return ToastAndroid.show(
|
|
"Something went wrong while downloading the file",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
|
|
const base64File = await FileSystem.readAsStringAsync(
|
|
downloadResult.uri,
|
|
{
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
},
|
|
);
|
|
|
|
await FileSystem.writeAsStringAsync(saveUri, base64File, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
});
|
|
|
|
ToastAndroid.show(
|
|
"Successfully downloaded the file",
|
|
ToastAndroid.SHORT,
|
|
);
|
|
}}
|
|
iconSize={20}
|
|
width={30}
|
|
height={30}
|
|
padding={5}
|
|
margin={{
|
|
left: 5,
|
|
right: 5,
|
|
}}
|
|
/>
|
|
</View>
|
|
</KeyboardAwareScrollView>
|
|
</View>
|
|
</Pressable>
|
|
</>
|
|
);
|
|
}
|