mirror of
https://github.com/Stef-00012/Zipline-Android-App.git
synced 2025-05-10 18:05:52 +02:00
Add folder uploads, add settings description, add metrics increase/decrease percentage
This commit is contained in:
parent
274343a5cf
commit
27edb6730f
30 changed files with 1270 additions and 225 deletions
|
@ -35,6 +35,7 @@ This will create an apk but won't automatically install
|
|||
|
||||
# TODO
|
||||
|
||||
- [ ] Add folder upload + upload button in folder view
|
||||
- [ ] Add description to settings like Zipline does
|
||||
- [ ] Add upload policy to folder large view
|
||||
- [x] Add folder upload + upload button in folder view
|
||||
- [x] Add description to settings like Zipline does
|
||||
- [x] Add upload policy to folder large view
|
||||
- [x] Add increase/decrease percetage in metrics
|
|
@ -106,6 +106,8 @@ export default function Files() {
|
|||
const url = db.get("url") as DashURL | null;
|
||||
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [allowUploads, setAllowUploads] = useState<boolean>(false);
|
||||
const [folderId, setFolderId] = useState<string | null>(null);
|
||||
const [isFolder, setIsFolder] = useState<boolean>(false);
|
||||
|
||||
const [focusedFile, setFocusedFile] = useState<APIFile | null>(null);
|
||||
|
@ -153,6 +155,8 @@ export default function Files() {
|
|||
if (typeof folder === "string") return router.replace("/+not-found");
|
||||
|
||||
setName(folder.name);
|
||||
setAllowUploads(folder.allowUploads);
|
||||
setFolderId(folder.id);
|
||||
|
||||
setIsFolder(true);
|
||||
return setFiles({
|
||||
|
@ -566,49 +570,55 @@ export default function Files() {
|
|||
/>
|
||||
|
||||
{!name && (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setTagsMenuOpen(true);
|
||||
}}
|
||||
icon="sell"
|
||||
color="transparent"
|
||||
iconColor={files ? "#2d3f70" : "#2d3f7055"}
|
||||
borderColor="#222c47"
|
||||
borderWidth={2}
|
||||
iconSize={30}
|
||||
disabled={!files}
|
||||
padding={4}
|
||||
rippleColor="#283557"
|
||||
margin={{
|
||||
left: 2,
|
||||
right: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
router.replace("/upload/file");
|
||||
}}
|
||||
icon="upload-file"
|
||||
color="transparent"
|
||||
iconColor={files ? "#2d3f70" : "#2d3f7055"}
|
||||
borderColor="#222c47"
|
||||
borderWidth={2}
|
||||
iconSize={30}
|
||||
disabled={!files}
|
||||
padding={4}
|
||||
rippleColor="#283557"
|
||||
margin={{
|
||||
left: 2,
|
||||
right: 2,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setTagsMenuOpen(true);
|
||||
}}
|
||||
icon="sell"
|
||||
color="transparent"
|
||||
iconColor={files ? "#2d3f70" : "#2d3f7055"}
|
||||
borderColor="#222c47"
|
||||
borderWidth={2}
|
||||
iconSize={30}
|
||||
disabled={!files}
|
||||
padding={4}
|
||||
rippleColor="#283557"
|
||||
margin={{
|
||||
left: 2,
|
||||
right: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
const url = isFolder
|
||||
? (`/folders/upload?folderId=${folderId}` as `/folders/upload?folderId=${string | null}`)
|
||||
: "/upload/file";
|
||||
|
||||
router.replace(url);
|
||||
}}
|
||||
icon="upload-file"
|
||||
color="transparent"
|
||||
iconColor={
|
||||
(files && !isFolder) || (isFolder && allowUploads)
|
||||
? "#2d3f70"
|
||||
: "#2d3f7055"
|
||||
}
|
||||
borderColor="#222c47"
|
||||
borderWidth={2}
|
||||
iconSize={30}
|
||||
disabled={!files || (isFolder && !allowUploads)}
|
||||
padding={4}
|
||||
rippleColor="#283557"
|
||||
margin={{
|
||||
left: 2,
|
||||
right: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
db.set(
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function ServerSettings() {
|
|||
}, []);
|
||||
|
||||
const [saveSettings, setSaveSettings] = useState<SaveSettings | null>(null);
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
|
@ -223,7 +223,7 @@ export default function ServerSettings() {
|
|||
|
||||
async function handleSave(category: SaveCategories) {
|
||||
setSaveError(null);
|
||||
setSaving(true)
|
||||
setSaving(true);
|
||||
|
||||
if (!saveSettings) return setSaving(false);
|
||||
let settingsToSave: Partial<APISettings> = {};
|
||||
|
@ -463,30 +463,30 @@ export default function ServerSettings() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(settingsToSave).length <= 0) {
|
||||
setSaveError(["Something went wrong..."])
|
||||
return setSaving(false)
|
||||
setSaveError(["Something went wrong..."]);
|
||||
return setSaving(false);
|
||||
}
|
||||
|
||||
|
||||
const success = await updateSettings(settingsToSave);
|
||||
|
||||
|
||||
if (Array.isArray(success)) {
|
||||
setSaveError(success);
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
|
||||
const reloadSuccess = await reloadSettings();
|
||||
|
||||
|
||||
if (typeof reloadSuccess === "string") {
|
||||
setSaveError([`Error while reloading: ${reloadSuccess}`]);
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
const newSettings = await getSettings();
|
||||
|
||||
setSettings(typeof newSettings === "string" ? null : newSettings);
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
|
||||
return ToastAndroid.show(
|
||||
"Successfully saved the settings",
|
||||
|
@ -522,6 +522,12 @@ export default function ServerSettings() {
|
|||
>
|
||||
<Text style={styles.headerText}>{setting.name}</Text>
|
||||
|
||||
{setting.description && (
|
||||
<Text style={styles.headerDescription}>
|
||||
{setting.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{setting.children.map((childSetting) =>
|
||||
renderSetting(childSetting),
|
||||
)}
|
||||
|
@ -534,6 +540,7 @@ export default function ServerSettings() {
|
|||
<TextInput
|
||||
key={setting.setting}
|
||||
title={setting.name}
|
||||
description={setting.description}
|
||||
password={setting.passwordInput}
|
||||
disabled={saving}
|
||||
keyboardType={setting.keyboardType}
|
||||
|
@ -579,6 +586,7 @@ export default function ServerSettings() {
|
|||
title={setting.name}
|
||||
key={setting.setting}
|
||||
data={setting.options}
|
||||
description={setting.description}
|
||||
disabled={saving}
|
||||
onSelect={(selectedItem) => {
|
||||
setSaveSettings((prev) => {
|
||||
|
@ -601,6 +609,7 @@ export default function ServerSettings() {
|
|||
<Switch
|
||||
key={setting.setting}
|
||||
disabled={saving}
|
||||
description={setting.description}
|
||||
onValueChange={() =>
|
||||
setSaveSettings((prev) => {
|
||||
return {
|
||||
|
@ -625,7 +634,12 @@ export default function ServerSettings() {
|
|||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text style={styles.externalLinkTitle}>External Links</Text>
|
||||
<View>
|
||||
<Text style={styles.externalLinkTitle}>External Links</Text>
|
||||
<Text style={styles.externalLinkDescription}>
|
||||
The external links to show in the footer.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
color="#323ea8"
|
||||
|
@ -643,6 +657,7 @@ export default function ServerSettings() {
|
|||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
...styles.settingGroup,
|
||||
|
@ -739,6 +754,7 @@ export default function ServerSettings() {
|
|||
return (
|
||||
<ColorPicker
|
||||
title={setting.name}
|
||||
description={setting.description}
|
||||
disabled={saving}
|
||||
initialColor={saveSettings?.[setting.setting] as string | undefined}
|
||||
onSelectColor={(color) => {
|
||||
|
@ -774,15 +790,15 @@ export default function ServerSettings() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(saveError, !saveError || saveError.length <= 0)
|
||||
}, [saveError])
|
||||
console.log(saveError, !saveError || saveError.length <= 0);
|
||||
}, [saveError]);
|
||||
|
||||
return (
|
||||
<View style={styles.mainContainer}>
|
||||
<View style={styles.mainContainer}>
|
||||
<Popup
|
||||
onClose={() => {
|
||||
setSaveError(null)
|
||||
setSaveError(null);
|
||||
}}
|
||||
hidden={!saveError || saveError.length <= 0}
|
||||
>
|
||||
|
@ -791,7 +807,9 @@ export default function ServerSettings() {
|
|||
|
||||
<ScrollView style={styles.popupScrollView}>
|
||||
{saveError?.map((error) => (
|
||||
<Text key={error} style={styles.errorText}>{error}</Text>
|
||||
<Text key={error} style={styles.errorText}>
|
||||
{error}
|
||||
</Text>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
|
@ -801,9 +819,6 @@ export default function ServerSettings() {
|
|||
</View>
|
||||
</Popup>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* <Popup
|
||||
onClose={() => {
|
||||
setSaveError(null)
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
export type FolderActions =
|
||||
| "viewFiles"
|
||||
| "visibility"
|
||||
| "uploadPolicy"
|
||||
| "edit"
|
||||
| "copyUrl"
|
||||
| "delete";
|
||||
|
@ -66,7 +67,14 @@ export default function Folders() {
|
|||
const dashUrl = db.get("url") as DashURL | null;
|
||||
|
||||
const [sortKey, setSortKey] = useState<{
|
||||
id: "name" | "public" | "allowUploads" | "createdAt" | "updatedAt" | "files" | "id";
|
||||
id:
|
||||
| "name"
|
||||
| "public"
|
||||
| "allowUploads"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "files"
|
||||
| "id";
|
||||
sortOrder: "asc" | "desc";
|
||||
}>({
|
||||
id: "createdAt",
|
||||
|
@ -133,8 +141,8 @@ export default function Folders() {
|
|||
ToastAndroid.SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
case "uploasPolicy": {
|
||||
|
||||
case "uploadPolicy": {
|
||||
const folderId = folder.id;
|
||||
|
||||
const success = await editFolder(folderId, {
|
||||
|
@ -413,9 +421,9 @@ export default function Folders() {
|
|||
sortable: true,
|
||||
},
|
||||
{
|
||||
row: "Uploads?",
|
||||
id: "allowUploads",
|
||||
sortable: true,
|
||||
row: "Uploads?",
|
||||
id: "allowUploads",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
row: "Created",
|
||||
|
@ -524,7 +532,7 @@ export default function Folders() {
|
|||
{folder.public ? "Yes" : "No"}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
const allowUploads = (
|
||||
<Text key={folder.id} style={styles.rowText}>
|
||||
{folder.allowUploads ? "Yes" : "No"}
|
||||
|
@ -600,16 +608,18 @@ export default function Folders() {
|
|||
onAction("visibility", folder);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
icon={folder.allowUploads ? "block" : "share"}
|
||||
color={folder.allowUploads ? "#323ea8" : "#343a40"}
|
||||
color={
|
||||
folder.allowUploads ? "#323ea8" : "#343a40"
|
||||
}
|
||||
iconSize={20}
|
||||
width={32}
|
||||
height={32}
|
||||
padding={6}
|
||||
onPress={async () => {
|
||||
onAction("uploasPolicy", folder);
|
||||
onAction("uploadPolicy", folder);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,22 +1,540 @@
|
|||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { getFolder, getFolders } from "@/functions/zipline/folders";
|
||||
import {
|
||||
type ExternalPathString,
|
||||
Link,
|
||||
useLocalSearchParams,
|
||||
useRouter,
|
||||
} from "expo-router";
|
||||
import { getFolder } from "@/functions/zipline/folders";
|
||||
import { ScrollView, Text, ToastAndroid, View } from "react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { APIFolder, APIUploadResponse, DashURL } from "@/types/zipline";
|
||||
import type { SelectedFile } from "@/app/(app)/(files)/upload/file";
|
||||
import { uploadFiles, type UploadFileOptions } from "@/functions/zipline/files";
|
||||
import { getSettings } from "@/functions/zipline/settings";
|
||||
import { useDetectKeyboardOpen } from "@/hooks/isKeyboardOpen";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import Popup from "@/components/Popup";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import Button from "@/components/Button";
|
||||
import FileDisplay from "@/components/FileDisplay";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
||||
import Select from "@/components/Select";
|
||||
import { dates, formats } from "@/constants/upload";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import Switch from "@/components/Switch";
|
||||
import { guessExtension } from "@/functions/util";
|
||||
import type { Mimetypes } from "@/types/mimetypes";
|
||||
import { styles } from "@/styles/folders/upload";
|
||||
import * as db from "@/functions/database";
|
||||
|
||||
export default function FolderUpload() {
|
||||
const searchParams = useLocalSearchParams<{
|
||||
folderId?: string;
|
||||
}>();
|
||||
|
||||
const [folder, setFolder] = useState<APIFolder>(null)
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const folder = await getFolder(searchParams.folderId)
|
||||
|
||||
setFolder(typeof folder === "string" ? null : folder)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
)
|
||||
}
|
||||
const router = useRouter();
|
||||
|
||||
const searchParams = useLocalSearchParams<{
|
||||
folderId?: string;
|
||||
}>();
|
||||
|
||||
const url = db.get("url") as DashURL | null;
|
||||
|
||||
const [folder, setFolder] = useState<APIFolder | null>(null);
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<Array<SelectedFile>>([]);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<
|
||||
APIUploadResponse["files"]
|
||||
>([]);
|
||||
const [failedUploads, setFailedUploads] = useState<
|
||||
Array<{
|
||||
uri: string;
|
||||
name: string;
|
||||
error: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const [overrideDomain, setOverrideDomain] =
|
||||
useState<UploadFileOptions["overrideDomain"]>();
|
||||
const [originalName, setOriginalName] =
|
||||
useState<UploadFileOptions["originalName"]>(false);
|
||||
const [compression, setCompression] =
|
||||
useState<UploadFileOptions["compression"]>();
|
||||
const [deletesAt, setDeletesAt] = useState<UploadFileOptions["expiresAt"]>();
|
||||
const [nameFormat, setNameFormat] = useState<UploadFileOptions["format"]>();
|
||||
const [maxViews, setMaxViews] = useState<UploadFileOptions["maxViews"]>();
|
||||
const [fileName, setFileName] = useState<UploadFileOptions["filename"]>();
|
||||
const [password, setPassword] = useState<UploadFileOptions["password"]>();
|
||||
const [defaultFormat, setDefaultFormat] = useState<string>("random");
|
||||
|
||||
const [uploadButtonDisabled, setUploadButtonDisabled] =
|
||||
useState<boolean>(true);
|
||||
const [fileNameEnabled, setFileNameEnabled] = useState<boolean>(true);
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [uploadPercentage, setUploadPercentage] = useState<string>("0");
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: .
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!searchParams.folderId) return router.replace("/+not-found");
|
||||
|
||||
const folder = await getFolder(searchParams.folderId);
|
||||
|
||||
if (typeof folder === "string") return router.replace("/+not-found");
|
||||
|
||||
if (!folder.allowUploads) return router.replace("/+not-found");
|
||||
|
||||
setFolder(folder);
|
||||
|
||||
const settings = await getSettings();
|
||||
|
||||
if (typeof settings !== "string") {
|
||||
setDefaultFormat(settings.filesDefaultFormat);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
function afterUploadCleanup() {
|
||||
setUploading(false);
|
||||
setSelectedFiles([]);
|
||||
|
||||
setMaxViews(undefined);
|
||||
setFileName(undefined);
|
||||
setPassword(undefined);
|
||||
setOriginalName(false);
|
||||
setDeletesAt(undefined);
|
||||
setNameFormat(undefined);
|
||||
setCompression(undefined);
|
||||
setOverrideDomain(undefined);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setUploadButtonDisabled(selectedFiles.length === 0);
|
||||
setFileNameEnabled(selectedFiles.length <= 1);
|
||||
}, [selectedFiles]);
|
||||
|
||||
const iskeyboardOpen = useDetectKeyboardOpen(false);
|
||||
|
||||
return (
|
||||
<View style={styles.mainContainer}>
|
||||
<Popup
|
||||
onClose={() => {
|
||||
setUploadedFiles([]);
|
||||
setFailedUploads([]);
|
||||
}}
|
||||
hidden={!(uploadedFiles.length > 0 || failedUploads.length > 0)}
|
||||
>
|
||||
{uploadedFiles.length > 0 && (
|
||||
<View>
|
||||
<Text style={styles.headerText}>Uploaded Files</Text>
|
||||
<ScrollView style={styles.popupScrollView}>
|
||||
{uploadedFiles.map((file) => (
|
||||
<View key={file.id} style={styles.uploadedFileContainer}>
|
||||
<Link
|
||||
href={file.url as ExternalPathString}
|
||||
style={styles.uploadedFileUrl}
|
||||
>
|
||||
{file.url}
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
icon="content-copy"
|
||||
color="#323ea8"
|
||||
onPress={async () => {
|
||||
const saved = await Clipboard.setStringAsync(file.url);
|
||||
|
||||
if (saved)
|
||||
return ToastAndroid.show(
|
||||
"URL copied to clipboard",
|
||||
ToastAndroid.SHORT,
|
||||
);
|
||||
|
||||
ToastAndroid.show(
|
||||
"Failed to copy the URL",
|
||||
ToastAndroid.SHORT,
|
||||
);
|
||||
}}
|
||||
iconSize={20}
|
||||
width={32}
|
||||
height={32}
|
||||
padding={6}
|
||||
margin={{
|
||||
left: 5,
|
||||
right: 5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="open-in-new"
|
||||
color="#323ea8"
|
||||
onPress={() => {
|
||||
router.replace(file.url as ExternalPathString);
|
||||
}}
|
||||
iconSize={20}
|
||||
width={32}
|
||||
height={32}
|
||||
padding={6}
|
||||
margin={{
|
||||
left: 5,
|
||||
right: 5,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{failedUploads.length > 0 && (
|
||||
<View
|
||||
style={{
|
||||
...(uploadedFiles.length > 0 && {
|
||||
marginTop: 10,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Text style={styles.headerText}>Failed Files</Text>
|
||||
<ScrollView style={styles.popupScrollView}>
|
||||
{failedUploads.map((file) => (
|
||||
<Text key={file.uri} style={styles.failedFileText}>
|
||||
<Text style={{ fontWeight: "bold" }}>{file.name}</Text>:{" "}
|
||||
<Text style={{ color: "red" }}>{file.error}</Text>
|
||||
</Text>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text
|
||||
style={{
|
||||
...styles.subHeaderText,
|
||||
...styles.popupSubHeaderText,
|
||||
}}
|
||||
>
|
||||
Press outside to close this popup
|
||||
</Text>
|
||||
</Popup>
|
||||
|
||||
<View>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.headerText}>
|
||||
Selected Files for {folder?.name}
|
||||
</Text>
|
||||
<Text style={styles.subHeaderText}>Click a file to remove it</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{
|
||||
...styles.scrollView,
|
||||
...(iskeyboardOpen && styles.scrollViewKeyboardOpen),
|
||||
}}
|
||||
>
|
||||
{selectedFiles.map((file) => (
|
||||
<View key={file.uri} style={styles.recentFileContainer}>
|
||||
<FileDisplay
|
||||
uri={file.uri}
|
||||
name={file.name}
|
||||
width={200}
|
||||
height={200}
|
||||
mimetype={file.mimetype}
|
||||
openable={false}
|
||||
onPress={() => {
|
||||
if (uploading) return;
|
||||
setSelectedFiles((alreadySelectedFiles) =>
|
||||
alreadySelectedFiles.filter(
|
||||
(selectedFile) => selectedFile.uri !== file.uri,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Button
|
||||
width="90%"
|
||||
disabled={uploading}
|
||||
onPress={async () => {
|
||||
const output = await DocumentPicker.getDocumentAsync({
|
||||
type: "*/*",
|
||||
multiple: true,
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
|
||||
if (output.canceled || !output.assets) return;
|
||||
|
||||
const newSelectedFiles: Array<SelectedFile> = output.assets
|
||||
.map((file) => ({
|
||||
name: file.name,
|
||||
uri: file.uri,
|
||||
mimetype: file.mimeType,
|
||||
size: file.size,
|
||||
}))
|
||||
.filter(
|
||||
(newFile) =>
|
||||
!selectedFiles.find(
|
||||
(selectedFile) =>
|
||||
newFile.size === selectedFile.size &&
|
||||
newFile.name === selectedFile.name,
|
||||
),
|
||||
);
|
||||
|
||||
setSelectedFiles((alreadySelectedFiles) => [
|
||||
...alreadySelectedFiles,
|
||||
...newSelectedFiles,
|
||||
]);
|
||||
}}
|
||||
text="Select File(s)"
|
||||
color={uploading ? "#373d79" : "#323ea8"}
|
||||
textColor={uploading ? "gray" : "white"}
|
||||
margin={{
|
||||
left: "auto",
|
||||
right: "auto",
|
||||
top: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
{folder?.public ? (
|
||||
<Text style={styles.folderStatusText}>
|
||||
This folder is{" "}
|
||||
<Link
|
||||
style={styles.folderStatusLink}
|
||||
href={`${url}/folder/${folder?.id}` as ExternalPathString}
|
||||
>
|
||||
public
|
||||
</Link>
|
||||
. Anyone with the link can view its contents and upload files.
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.folderStatusText}>
|
||||
Only the owner can view this folder's contents. However, anyone can
|
||||
upload files, and they can still access their uploaded files if they
|
||||
have the link to the specific file.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<KeyboardAwareScrollView
|
||||
keyboardShouldPersistTaps="always"
|
||||
style={styles.inputsContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(uploading && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
Deletes At:
|
||||
</Text>
|
||||
<Select
|
||||
data={dates}
|
||||
placeholder="Select Date..."
|
||||
disabled={uploading}
|
||||
onSelect={(selectedDate) => {
|
||||
if (selectedDate.length <= 0) return;
|
||||
|
||||
if (selectedDate[0].value === "never")
|
||||
return setDeletesAt(undefined);
|
||||
|
||||
const deletesDate = new Date(
|
||||
Date.now() + (selectedDate[0].milliseconds as number),
|
||||
);
|
||||
|
||||
setDeletesAt(deletesDate);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(uploading && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
Format:
|
||||
</Text>
|
||||
<Select
|
||||
data={[
|
||||
{
|
||||
label: `Default (${defaultFormat})`,
|
||||
value: defaultFormat || "random",
|
||||
},
|
||||
...formats,
|
||||
]}
|
||||
disabled={uploading}
|
||||
placeholder="Select Format..."
|
||||
onSelect={(selectedFormat) => {
|
||||
if (selectedFormat.length <= 0) return;
|
||||
|
||||
setNameFormat(
|
||||
selectedFormat[0].value as UploadFileOptions["format"],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="Compression:"
|
||||
onValueChange={(content) => {
|
||||
let compressionPercentage = Number.parseInt(content);
|
||||
|
||||
if (compressionPercentage > 100) compressionPercentage = 100;
|
||||
if (compressionPercentage < 0) compressionPercentage = 0;
|
||||
|
||||
setCompression(compressionPercentage);
|
||||
}}
|
||||
keyboardType="numeric"
|
||||
disableContext={uploading}
|
||||
disabled={uploading}
|
||||
value={compression ? String(compression) : ""}
|
||||
placeholder="0"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="Max Views:"
|
||||
onValueChange={(content) => {
|
||||
let maxViewsAmount = Number.parseInt(content);
|
||||
|
||||
if (maxViewsAmount < 0) maxViewsAmount = 0;
|
||||
|
||||
setMaxViews(maxViewsAmount);
|
||||
}}
|
||||
keyboardType="numeric"
|
||||
disableContext={uploading}
|
||||
disabled={uploading}
|
||||
value={maxViews ? String(maxViews) : ""}
|
||||
placeholder="0"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="Override Domain:"
|
||||
onValueChange={(content) => setOverrideDomain(content)}
|
||||
keyboardType="url"
|
||||
disableContext={uploading}
|
||||
disabled={uploading}
|
||||
value={overrideDomain || ""}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="Override File Name:"
|
||||
disableContext={fileNameEnabled || !uploading}
|
||||
disabled={!fileNameEnabled || uploading}
|
||||
onValueChange={(content) => setFileName(content)}
|
||||
value={fileNameEnabled ? fileName || "" : ""}
|
||||
placeholder="example.png"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="Password:"
|
||||
onValueChange={(content) => setPassword(content)}
|
||||
disableContext={uploading}
|
||||
disabled={uploading}
|
||||
password
|
||||
value={password || ""}
|
||||
placeholder="myPassword"
|
||||
/>
|
||||
|
||||
<Switch
|
||||
title="Add Original Name"
|
||||
value={originalName || false}
|
||||
disabled={uploading}
|
||||
onValueChange={() => setOriginalName((prev) => !prev)}
|
||||
/>
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View>
|
||||
<Button
|
||||
width="90%"
|
||||
disabled={uploading || uploadButtonDisabled}
|
||||
onPress={async () => {
|
||||
setUploading(true);
|
||||
|
||||
const successful = [];
|
||||
const fails: typeof failedUploads = [];
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
const fileInfo = await FileSystem.getInfoAsync(file.uri, {
|
||||
size: true,
|
||||
});
|
||||
|
||||
if (!fileInfo.exists || fileInfo.isDirectory) {
|
||||
fails.push({
|
||||
error: "File does not exist",
|
||||
uri: file.uri,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const mimetype =
|
||||
file.mimetype ||
|
||||
guessExtension(
|
||||
file.uri.split(".").pop() as Mimetypes[keyof Mimetypes],
|
||||
);
|
||||
|
||||
const fileData = {
|
||||
uri: fileInfo.uri || file.uri,
|
||||
mimetype,
|
||||
};
|
||||
|
||||
const uploadedFile = await uploadFiles(
|
||||
fileData,
|
||||
{
|
||||
compression,
|
||||
expiresAt: deletesAt,
|
||||
filename: fileNameEnabled ? fileName : undefined,
|
||||
folder: searchParams.folderId,
|
||||
format: nameFormat,
|
||||
maxViews,
|
||||
originalName,
|
||||
overrideDomain,
|
||||
password,
|
||||
},
|
||||
(uploadData) => {
|
||||
setUploadPercentage(
|
||||
(
|
||||
(uploadData.totalBytesSent /
|
||||
uploadData.totalBytesExpectedToSend) *
|
||||
100
|
||||
).toFixed(2),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof uploadedFile === "string") {
|
||||
fails.push({
|
||||
error: uploadedFile,
|
||||
uri: file.uri,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
successful.push(uploadedFile[0]);
|
||||
}
|
||||
|
||||
setUploadedFiles(successful);
|
||||
setFailedUploads(fails);
|
||||
|
||||
afterUploadCleanup();
|
||||
}}
|
||||
text={
|
||||
uploading ? `Uploading... ${uploadPercentage}%` : "Upload File(s)"
|
||||
}
|
||||
color={uploading || uploadButtonDisabled ? "#373d79" : "#323ea8"}
|
||||
textColor={uploading || uploadButtonDisabled ? "gray" : "white"}
|
||||
margin={{
|
||||
left: "auto",
|
||||
right: "auto",
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { Dimensions, ScrollView, Text, View } from "react-native";
|
||||
import { LineChart, PieChart } from "react-native-gifted-charts";
|
||||
import { colorHash, convertToBytes } from "@/functions/util";
|
||||
import {
|
||||
colorHash,
|
||||
convertToBytes,
|
||||
getMetricsDifference,
|
||||
} from "@/functions/util";
|
||||
import type { DateType } from "react-native-ui-datepicker";
|
||||
import { getSettings } from "@/functions/zipline/settings";
|
||||
import { useShareIntent } from "@/hooks/useShareIntent";
|
||||
|
@ -19,6 +23,7 @@ import {
|
|||
getStats,
|
||||
type StatsProps,
|
||||
} from "@/functions/zipline/stats";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
|
||||
export default function Metrics() {
|
||||
useAuth();
|
||||
|
@ -29,6 +34,21 @@ export default function Metrics() {
|
|||
useState<boolean>(false);
|
||||
const [filteredStats, setFilteredStats] = useState<APIStats | null>(null);
|
||||
const [mainStat, setMainStat] = useState<APIStats[0] | null>(null);
|
||||
const [statsDifferences, setStatsDifferences] = useState<{
|
||||
files: number;
|
||||
urls: number;
|
||||
storage: number;
|
||||
users: number;
|
||||
fileViews: number;
|
||||
urlViews: number;
|
||||
}>({
|
||||
files: 0,
|
||||
fileViews: 0,
|
||||
storage: 0,
|
||||
urls: 0,
|
||||
urlViews: 0,
|
||||
users: 0,
|
||||
});
|
||||
|
||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
||||
const [allTime, setAllTime] = useState<boolean>(false);
|
||||
|
@ -87,6 +107,20 @@ export default function Metrics() {
|
|||
});
|
||||
|
||||
setStats(sortedStats);
|
||||
|
||||
const firstStat = sortedStats[sortedStats.length - 1].data;
|
||||
const lastStat = sortedStats[0].data;
|
||||
|
||||
const statsDiff = {
|
||||
files: getMetricsDifference(firstStat.files, lastStat.files),
|
||||
fileViews: getMetricsDifference(firstStat.fileViews, lastStat.fileViews),
|
||||
storage: getMetricsDifference(firstStat.storage, lastStat.storage),
|
||||
urls: getMetricsDifference(firstStat.urls, lastStat.urls),
|
||||
urlViews: getMetricsDifference(firstStat.urlViews, lastStat.urlViews),
|
||||
users: getMetricsDifference(firstStat.users, lastStat.users),
|
||||
};
|
||||
|
||||
setStatsDifferences(statsDiff);
|
||||
}
|
||||
|
||||
const windowWidth = Dimensions.get("window").width;
|
||||
|
@ -179,47 +213,96 @@ export default function Metrics() {
|
|||
|
||||
{filteredStats && mainStat ? (
|
||||
<View>
|
||||
<ScrollView style={{height: "93%"}}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{
|
||||
...styles.scrollView,
|
||||
...styles.statsContainer,
|
||||
}}
|
||||
>
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>Files:</Text>
|
||||
<Text style={styles.statText}>{mainStat.data.files}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>URLs:</Text>
|
||||
<Text style={styles.statText}>{mainStat.data.urls}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>Storage Used:</Text>
|
||||
<Text style={styles.statText}>
|
||||
{convertToBytes(mainStat.data.storage, {
|
||||
<ScrollView style={{ height: "93%" }}>
|
||||
<ScrollView horizontal style={styles.scrollView}>
|
||||
{[
|
||||
{
|
||||
title: "Files:",
|
||||
amount: mainStat.data.files,
|
||||
difference: statsDifferences.files,
|
||||
},
|
||||
{
|
||||
title: "URLs",
|
||||
amount: mainStat.data.urls,
|
||||
difference: statsDifferences.urls,
|
||||
},
|
||||
{
|
||||
title: "Storage Used:",
|
||||
amount: convertToBytes(mainStat.data.storage, {
|
||||
unitSeparator: " ",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
}),
|
||||
difference: statsDifferences.storage,
|
||||
},
|
||||
{
|
||||
title: "Users:",
|
||||
amount: mainStat.data.users,
|
||||
difference: statsDifferences.users,
|
||||
},
|
||||
{
|
||||
title: "File Views:",
|
||||
amount: mainStat.data.fileViews,
|
||||
difference: statsDifferences.fileViews,
|
||||
},
|
||||
{
|
||||
title: "URL Views:",
|
||||
amount: mainStat.data.urlViews,
|
||||
difference: statsDifferences.urlViews,
|
||||
},
|
||||
].map((stat) => (
|
||||
<View key={stat.title} style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>{stat.title}</Text>
|
||||
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>Users:</Text>
|
||||
<Text style={styles.statText}>{mainStat.data.users}</Text>
|
||||
</View>
|
||||
<View style={styles.statContainerData}>
|
||||
<Text style={styles.statText}>{stat.amount}</Text>
|
||||
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>File Views:</Text>
|
||||
<Text style={styles.statText}>{mainStat.data.fileViews}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
...styles.statDifferenceContainer,
|
||||
backgroundColor:
|
||||
stat.difference === 0
|
||||
? "#868E9640"
|
||||
: stat.difference > 0
|
||||
? "#40C05740"
|
||||
: "#FA525240",
|
||||
}}
|
||||
>
|
||||
{stat.difference > 0 ? (
|
||||
<MaterialIcons
|
||||
name="north"
|
||||
size={18}
|
||||
color="#69db7c"
|
||||
/>
|
||||
) : stat.difference < 0 ? (
|
||||
<MaterialIcons
|
||||
name="south"
|
||||
size={18}
|
||||
color="#ff8787"
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcons
|
||||
name="remove"
|
||||
size={18}
|
||||
color="#ced4da"
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.statContainer}>
|
||||
<Text style={styles.subHeaderText}>URL Views:</Text>
|
||||
<Text style={styles.statText}>{mainStat.data.urlViews}</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
...styles.statDifferenceText,
|
||||
color:
|
||||
stat.difference === 0
|
||||
? "#ced4da"
|
||||
: stat.difference > 0
|
||||
? "#69db7c"
|
||||
: "#ff8787",
|
||||
}}
|
||||
>
|
||||
{stat.difference}%
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{userSpecificMetrics && (
|
||||
|
|
|
@ -118,7 +118,7 @@ export default function UserSettings() {
|
|||
const [generateThumbnailsRerun, setGenerateThumbnailsRerun] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [ziplineVersion, setZiplineVersion] = useState<string | null>(null)
|
||||
const [ziplineVersion, setZiplineVersion] = useState<string | null>(null);
|
||||
|
||||
const url = db.get("url") as DashURL;
|
||||
|
||||
|
@ -129,7 +129,7 @@ export default function UserSettings() {
|
|||
const avatar = await getCurrentUserAvatar();
|
||||
const exports = await getUserExports();
|
||||
const zeroByteFiles = await getZeroByteFiles();
|
||||
const versionData = await getVersion()
|
||||
const versionData = await getVersion();
|
||||
|
||||
setUser(typeof user === "string" ? null : user);
|
||||
setToken(typeof token === "string" ? null : token.token);
|
||||
|
@ -138,7 +138,9 @@ export default function UserSettings() {
|
|||
setZeroByteFiles(
|
||||
typeof zeroByteFiles === "string" ? 0 : zeroByteFiles.files.length,
|
||||
);
|
||||
setZiplineVersion(typeof versionData === "string" ? null : versionData.version)
|
||||
setZiplineVersion(
|
||||
typeof versionData === "string" ? null : versionData.version,
|
||||
);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
|
@ -526,6 +528,7 @@ export default function UserSettings() {
|
|||
placeholder="myPassword123"
|
||||
value={password || ""}
|
||||
password
|
||||
description="Leave blank to keep the same password"
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
@ -685,11 +688,15 @@ export default function UserSettings() {
|
|||
{/* Viewing Files */}
|
||||
<View style={styles.settingGroup}>
|
||||
<Text style={styles.headerText}>Viewing Files</Text>
|
||||
<Text style={styles.headerDescription}>
|
||||
All text fields support using variables.
|
||||
</Text>
|
||||
|
||||
<Switch
|
||||
title="Enable View Routes"
|
||||
value={viewEnabled || false}
|
||||
onValueChange={() => setViewEnabled((prev) => !prev)}
|
||||
description="Enable viewing files through customizable view-routes"
|
||||
/>
|
||||
|
||||
<Switch
|
||||
|
@ -697,10 +704,12 @@ export default function UserSettings() {
|
|||
disabled={!viewEnabled}
|
||||
value={viewShowMimetype || false}
|
||||
onValueChange={() => setViewShowMimetype((prev) => !prev)}
|
||||
description="Show the mimetype of the file in the view-route"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
title="View Content:"
|
||||
description="Change the content within view-routes. Most HTML is valid, while the use of JavaScript is unavailable."
|
||||
disabled={!viewEnabled}
|
||||
disableContext={!viewEnabled}
|
||||
multiline
|
||||
|
@ -710,15 +719,9 @@ export default function UserSettings() {
|
|||
placeholder="This is my file"
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!viewEnabled && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
View Content Alignment:
|
||||
</Text>
|
||||
<Select
|
||||
title="View Content Alignment:"
|
||||
description="Change the alignment of the content within view-routes"
|
||||
disabled={!viewEnabled}
|
||||
data={alignments}
|
||||
onSelect={(selectedAlignment) => {
|
||||
|
@ -736,6 +739,7 @@ export default function UserSettings() {
|
|||
|
||||
<Switch
|
||||
title="Embed"
|
||||
description="Enable the following embed properties. These properties take advantage of OpenGraph tags. View routes will need to be enabled for this to work."
|
||||
disabled={!viewEmbed || !viewEnabled}
|
||||
value={viewEmbed || false}
|
||||
onValueChange={() => setViewEmbed((prev) => !prev)}
|
||||
|
@ -1016,6 +1020,9 @@ export default function UserSettings() {
|
|||
{/* Server Actions */}
|
||||
<View style={styles.settingGroup}>
|
||||
<Text style={styles.headerText}>Server Actions</Text>
|
||||
<Text style={styles.headerDescription}>
|
||||
Helpful scripts and tools for server management.
|
||||
</Text>
|
||||
|
||||
<View style={styles.serverActionButtonRow}>
|
||||
<Button
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function Home() {
|
|||
// // db.del("url")
|
||||
// // db.del("token")
|
||||
|
||||
// router.replace("/test");
|
||||
// // router.replace("/test");
|
||||
// }
|
||||
// });
|
||||
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -40,6 +40,7 @@ interface CustomColorPickerProps {
|
|||
initialColor?: string;
|
||||
showOpacity?: boolean;
|
||||
showPreview?: boolean;
|
||||
description?: string;
|
||||
showInput?: boolean;
|
||||
disabled?: boolean;
|
||||
showHue?: boolean;
|
||||
|
@ -85,6 +86,7 @@ export default function ColorPicker({
|
|||
previewHideText,
|
||||
showHue = true,
|
||||
onSelectColor,
|
||||
description,
|
||||
title,
|
||||
panel,
|
||||
}: CustomColorPickerProps) {
|
||||
|
@ -116,14 +118,23 @@ export default function ColorPicker({
|
|||
return (
|
||||
<>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(disabled && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!description && {
|
||||
marginBottom: 5,
|
||||
}),
|
||||
...(disabled && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.inputDescription}>{description}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Pressable
|
||||
disabled={disabled}
|
||||
|
|
|
@ -49,6 +49,16 @@ export default function LargeFolderView({ folder, dashUrl, onAction }: Props) {
|
|||
onAction("visibility", folder);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: folder.allowUploads
|
||||
? "Deny Anonymous Uploads"
|
||||
: "Allow Anonymous Uploads",
|
||||
id: `${folder.id}-uploadPolicy`,
|
||||
icon: folder.allowUploads ? "share" : "block",
|
||||
onPress: async () => {
|
||||
onAction("uploadPolicy", folder);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Edit Name",
|
||||
id: `${folder.id}-editName`,
|
||||
|
@ -76,6 +86,9 @@ export default function LargeFolderView({ folder, dashUrl, onAction }: Props) {
|
|||
},
|
||||
},
|
||||
]}
|
||||
dropdown={{
|
||||
width: 220,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
@ -97,14 +110,18 @@ export default function LargeFolderView({ folder, dashUrl, onAction }: Props) {
|
|||
<Text>{folder.public ? "Yes" : "No"}</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.key}>
|
||||
<Text style={styles.keyName}>Allow Anonymous Uploads</Text>:{" "}
|
||||
<Text>{folder.allowUploads ? "Yes" : "No"}</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.key}>
|
||||
<Text style={styles.keyName}>Files</Text>:{" "}
|
||||
<Text>{folder.files.length}</Text>
|
||||
</Text>
|
||||
|
||||
|
||||
<Text style={styles.key}>
|
||||
<Text style={styles.keyName}>ID</Text>:{" "}
|
||||
<Text>{folder.id}</Text>
|
||||
<Text style={styles.keyName}>ID</Text>: <Text>{folder.id}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -29,7 +29,10 @@ export interface SelectProps {
|
|||
item: SelectProps["data"][0],
|
||||
key: string,
|
||||
) => ReactNode | string;
|
||||
renderItem?: (item: SelectProps["data"][0], closeSelect: () => void) => ReactNode;
|
||||
renderItem?: (
|
||||
item: SelectProps["data"][0],
|
||||
closeSelect: () => void,
|
||||
) => ReactNode;
|
||||
id?: string;
|
||||
width?: DimensionValue;
|
||||
margin?: {
|
||||
|
@ -38,7 +41,8 @@ export interface SelectProps {
|
|||
left?: DimensionValue;
|
||||
right?: DimensionValue;
|
||||
};
|
||||
title?: string
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function Select({
|
||||
|
@ -53,6 +57,7 @@ export default function Select({
|
|||
multiple = false,
|
||||
width,
|
||||
title,
|
||||
description,
|
||||
margin = {},
|
||||
renderSelectedItem = (item, key) => (
|
||||
<Text
|
||||
|
@ -125,14 +130,23 @@ export default function Select({
|
|||
}}
|
||||
>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(disabled && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!description && {
|
||||
marginBottom: 5,
|
||||
}),
|
||||
...(disabled && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.inputDescription}>{description}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.selectButton}
|
||||
|
@ -209,7 +223,7 @@ export default function Select({
|
|||
/>
|
||||
)}
|
||||
{renderItem(item, () => {
|
||||
setIsOpen(false)
|
||||
setIsOpen(false);
|
||||
})}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { View } from "react-native";
|
|||
interface Props {
|
||||
value: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
onValueChange: (value: boolean, id?: Props["id"]) => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
|
@ -14,6 +15,7 @@ interface Props {
|
|||
export default function Switch({
|
||||
value,
|
||||
title,
|
||||
description,
|
||||
onValueChange,
|
||||
disabled = false,
|
||||
id,
|
||||
|
@ -30,15 +32,22 @@ export default function Switch({
|
|||
false: "#181c28",
|
||||
}}
|
||||
/>
|
||||
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.switchText,
|
||||
...(disabled && styles.switchTextDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
style={{
|
||||
...styles.switchText,
|
||||
...(disabled && styles.switchTextDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.switchDescription}>{description}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,7 @@ interface Props {
|
|||
value?: string;
|
||||
defaultValue?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
onPasswordToggle?: (
|
||||
visibile: boolean,
|
||||
|
@ -60,6 +61,7 @@ export default function TextInput({
|
|||
disableContext = false,
|
||||
multiline = false,
|
||||
title,
|
||||
description,
|
||||
password = false,
|
||||
keyboardType = "default",
|
||||
placeholder,
|
||||
|
@ -81,14 +83,25 @@ export default function TextInput({
|
|||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(disabled && showDisabledStyle && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!description && {
|
||||
marginBottom: 5,
|
||||
}),
|
||||
...(disabled &&
|
||||
showDisabledStyle &&
|
||||
styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.inputDescription}>{description}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
|
@ -138,14 +151,25 @@ export default function TextInput({
|
|||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(disabled && showDisabledStyle && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!description && {
|
||||
marginBottom: 5,
|
||||
}),
|
||||
...(disabled &&
|
||||
showDisabledStyle &&
|
||||
styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.inputDescription}>{description}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
|
@ -192,14 +216,23 @@ export default function TextInput({
|
|||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(disabled && showDisabledStyle && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...styles.inputHeader,
|
||||
...(!description && {
|
||||
marginBottom: 5,
|
||||
}),
|
||||
...(disabled && showDisabledStyle && styles.inputHeaderDisabled),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && (
|
||||
<Text style={styles.inputDescription}>{description}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<NativeTextInput
|
||||
onChange={(event) => onChange(event, id)}
|
||||
|
|
|
@ -348,6 +348,7 @@ interface Category {
|
|||
| "OIDCOAuth"
|
||||
| "discordOnUploadEmbed"
|
||||
| "discordOnShortenEmbed"}Category`;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface Input {
|
||||
|
@ -360,6 +361,7 @@ interface Input {
|
|||
multiline?: boolean;
|
||||
type: "input";
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface Switch {
|
||||
|
@ -367,6 +369,7 @@ interface Switch {
|
|||
setting: SettingPath;
|
||||
type: "switch";
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Select {
|
||||
|
@ -377,12 +380,14 @@ interface Select {
|
|||
placeholder: string;
|
||||
type: "select";
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ColorPicker {
|
||||
type: "colorPicker";
|
||||
setting: SettingPath;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Save {
|
||||
|
@ -413,6 +418,7 @@ export const settings: Array<Setting> = [
|
|||
type: "switch",
|
||||
name: "Return HTTPS URls",
|
||||
setting: "coreReturnHttpsUrls",
|
||||
description: "Return URLs with HTTPS protocol.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -420,6 +426,8 @@ export const settings: Array<Setting> = [
|
|||
keyboardType: "url",
|
||||
setting: "coreDefaultDomain",
|
||||
placeholder: "example.com",
|
||||
description:
|
||||
"The domain to use when generating URLs. This value should not include the protocol.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -427,6 +435,8 @@ export const settings: Array<Setting> = [
|
|||
keyboardType: "default",
|
||||
setting: "coreTempDirectory",
|
||||
placeholder: "/tmp/zipline",
|
||||
description:
|
||||
"The directory to store temporary files. If the path is invalid, certain functions may break. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -439,6 +449,12 @@ export const settings: Array<Setting> = [
|
|||
name: "Chunks",
|
||||
category: "chunksCategory",
|
||||
children: [
|
||||
{
|
||||
type: "switch",
|
||||
name: "Enable Chunks",
|
||||
setting: "chunksEnabled",
|
||||
description: "Enable chunked uploads.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Max Chunks Size",
|
||||
|
@ -446,6 +462,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "chunksMax",
|
||||
placeholder: "95mb",
|
||||
displayType: "bytes",
|
||||
description:
|
||||
"Maximum size of an upload before it is split into chunks.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -454,6 +472,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "chunksSize",
|
||||
placeholder: "25mb",
|
||||
displayType: "bytes",
|
||||
description: "Size of each chunk.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -464,6 +483,7 @@ export const settings: Array<Setting> = [
|
|||
{
|
||||
type: "category",
|
||||
name: "Tasks",
|
||||
description: "All options require a restart to take effect.",
|
||||
category: "tasksCategory",
|
||||
children: [
|
||||
{
|
||||
|
@ -473,6 +493,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "tasksDeleteInterval",
|
||||
placeholder: "30m",
|
||||
displayType: "time",
|
||||
description: "How often to check and delete expired files.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -481,6 +502,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "tasksClearInvitesInterval",
|
||||
placeholder: "30m",
|
||||
displayType: "time",
|
||||
description: "How often to check and clear expired/used invites.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -489,6 +511,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "tasksMaxViewsInterval",
|
||||
placeholder: "30m",
|
||||
displayType: "time",
|
||||
description:
|
||||
"How often to check and delete files that have reached max views.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -497,6 +521,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "tasksThumbnailsInterval",
|
||||
placeholder: "5m",
|
||||
displayType: "time",
|
||||
description:
|
||||
"How often to check and generate thumbnails for video files.",
|
||||
},
|
||||
// {
|
||||
// type: "input",
|
||||
|
@ -521,11 +547,15 @@ export const settings: Array<Setting> = [
|
|||
type: "switch",
|
||||
name: "Passkeys",
|
||||
setting: "mfaPasskeys",
|
||||
description:
|
||||
"Enable the use of passwordless login with the use of WebAuthn passkeys like your phone, security keys, etc.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "TOTP Enabled",
|
||||
setting: "mfaTotpEnabled",
|
||||
description:
|
||||
"Enable Time-based One-Time Passwords with the use of an authenticator app.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -533,6 +563,7 @@ export const settings: Array<Setting> = [
|
|||
keyboardType: "default",
|
||||
setting: "mfaTotpIssuer",
|
||||
placeholder: "Zipline",
|
||||
description: "The issuer to use for the TOTP token.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -549,51 +580,67 @@ export const settings: Array<Setting> = [
|
|||
type: "switch",
|
||||
name: "Image Compression",
|
||||
setting: "featuresImageCompression",
|
||||
description: "Allows the ability for users to compress images.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "/robots.txt",
|
||||
setting: "featuresRobotsTxt",
|
||||
description:
|
||||
"Enables a robots.txt file for search engine optimization. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Healthcheck",
|
||||
setting: "featuresHealthcheck",
|
||||
description:
|
||||
"Enables a healthcheck route for uptime monitoring. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "User Registration",
|
||||
setting: "featuresUserRegistration",
|
||||
description: "Allows users to register an account on the server.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "OAuth Registration",
|
||||
setting: "featuresOauthRegistration",
|
||||
description:
|
||||
"Allows users to register an account using OAuth providers.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Delete on Max Views",
|
||||
setting: "featuresDeleteOnMaxViews",
|
||||
description:
|
||||
"Automatically deletes files/urls after they reach the maximum view count. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Enable Metrics",
|
||||
setting: "featuresMetricsEnabled",
|
||||
description:
|
||||
"Enables metrics for the server. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Admin Only Metrics",
|
||||
setting: "featuresMetricsAdminOnly",
|
||||
description: "Requires an administrator to view metrics.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Show User Specific Metrics",
|
||||
setting: "featuresMetricsShowUserSpecific",
|
||||
description: "Shows metrics specific to each user, for all users.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Enable Thumbnails",
|
||||
setting: "featuresThumbnailsEnabled",
|
||||
description:
|
||||
"Enables thumbnail generation for images. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -601,6 +648,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "featuresThumbnailsNumberThreads",
|
||||
keyboardType: "numeric",
|
||||
placeholder: "Enter a number...",
|
||||
description:
|
||||
"Number of threads to use for thumbnail generation, usually the number of CPU threads. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -618,22 +667,28 @@ export const settings: Array<Setting> = [
|
|||
name: "Route",
|
||||
setting: "filesRoute",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The route to use for file uploads. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Length",
|
||||
setting: "filesLength",
|
||||
keyboardType: "numeric",
|
||||
description:
|
||||
"The length of the file name (for randomly generated names).",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Assume Mimetypes",
|
||||
setting: "filesAssumeMimetypes",
|
||||
description: "Assume the mimetype of a file for its extension.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Remove GPS Metadata",
|
||||
setting: "filesRemoveGpsMetadata",
|
||||
description: "Remove GPS metadata from files.",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
|
@ -642,6 +697,7 @@ export const settings: Array<Setting> = [
|
|||
defaultValue: "random",
|
||||
setting: "filesDefaultFormat",
|
||||
placeholder: "Select format...",
|
||||
description: "The default format to use for file names.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -649,6 +705,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "filesDisabledExtensions",
|
||||
keyboardType: "default",
|
||||
joinString: ", ",
|
||||
description: "Extensions to disable, separated by commas.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -656,30 +713,36 @@ export const settings: Array<Setting> = [
|
|||
setting: "filesMaxFileSize",
|
||||
keyboardType: "default",
|
||||
displayType: "bytes",
|
||||
description: "The maximum file size allowed.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Default Expiration",
|
||||
setting: "filesDefaultExpiration",
|
||||
keyboardType: "default",
|
||||
description: "The default expiration time for files.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Default Date Format",
|
||||
setting: "filesDefaultDateFormat",
|
||||
keyboardType: "default",
|
||||
description: "The default date format to use.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Random Words Num Adjectives",
|
||||
setting: "filesRandomWordsNumAdjectives",
|
||||
keyboardType: "numeric",
|
||||
description:
|
||||
"The number of adjectives to use for the random-words/gfycat format.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Random Words Separator",
|
||||
setting: "filesRandomWordsSeparator",
|
||||
placeholder: "-",
|
||||
description: "The separator to use for the random-words/gfycat format.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -697,12 +760,16 @@ export const settings: Array<Setting> = [
|
|||
name: "Route",
|
||||
setting: "urlsRoute",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The route to use for short URLs. Requires a server restart.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Length",
|
||||
setting: "urlsLength",
|
||||
keyboardType: "numeric",
|
||||
description:
|
||||
"The length of the short URL (for randomly generated names).",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -719,12 +786,14 @@ export const settings: Array<Setting> = [
|
|||
type: "switch",
|
||||
name: "Enable Invites",
|
||||
setting: "invitesEnabled",
|
||||
description: "Enable the use of invite links to register new users.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Length",
|
||||
setting: "invitesLength",
|
||||
keyboardType: "numeric",
|
||||
description: "The length of the invite code.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -735,29 +804,35 @@ export const settings: Array<Setting> = [
|
|||
{
|
||||
type: "category",
|
||||
name: "Ratelimit",
|
||||
description: "All options require a restart to take effect.",
|
||||
category: "ratelimitCategory",
|
||||
children: [
|
||||
{
|
||||
type: "switch",
|
||||
name: "Enable Ratelimit",
|
||||
setting: "ratelimitEnabled",
|
||||
description: "Enable ratelimiting for the server.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Admin Bypass",
|
||||
setting: "ratelimitAdminBypass",
|
||||
description: "Allow admins to bypass the ratelimit.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Max Requests",
|
||||
setting: "ratelimitMax",
|
||||
keyboardType: "numeric",
|
||||
description:
|
||||
"The maximum number of requests allowed within the window. If no window is set, this is the maximum number of requests until it reaches the limit.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Window",
|
||||
setting: "ratelimitWindow",
|
||||
keyboardType: "numeric",
|
||||
description: "The window in seconds to allow the max requests.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -765,6 +840,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "ratelimitAllowList",
|
||||
joinString: ", ",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"A comma-separated list of IP addresses to bypass the ratelimit.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -783,56 +860,70 @@ export const settings: Array<Setting> = [
|
|||
name: "Title",
|
||||
setting: "websiteTitle",
|
||||
keyboardType: "default",
|
||||
description: "The title of the website in browser tabs and at the top.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Title Logo",
|
||||
setting: "websiteTitleLogo",
|
||||
keyboardType: "url",
|
||||
description:
|
||||
"The URL to use for the title logo. This is placed to the left of the title.",
|
||||
},
|
||||
{
|
||||
type: "externalUrls",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Login Backgorund",
|
||||
name: "Login Background",
|
||||
setting: "websiteLoginBackground",
|
||||
keyboardType: "url",
|
||||
description: "The URL to use for the login background.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Login Backgorund Blur",
|
||||
name: "Login Background Blur",
|
||||
setting: "websiteLoginBackgroundBlur",
|
||||
description: "Whether to blur the login background.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Default Avatar",
|
||||
setting: "websiteDefaultAvatar",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The path to use for the default avatar. This must be a path to an image, not a URL.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Terms of Service",
|
||||
setting: "websiteTos",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"Path to a Markdown (.md) file to use for the terms of service.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Default Theme",
|
||||
setting: "websiteThemeDefault",
|
||||
keyboardType: "default",
|
||||
description: "The default theme to use for the website.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Dark Theme",
|
||||
setting: "websiteThemeDark",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
'The dark theme to use for the website when the default theme is "system".',
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Light Theme",
|
||||
setting: "websiteThemeLight",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
'The light theme to use for the website when the default theme is "system".',
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -843,17 +934,23 @@ export const settings: Array<Setting> = [
|
|||
{
|
||||
type: "category",
|
||||
name: "OAuth",
|
||||
description:
|
||||
'For OAuth to work, the "OAuth Registration" setting must be enabled in the Features section. If you have issues, try restarting Zipline after saving.',
|
||||
category: "oauthCategory",
|
||||
children: [
|
||||
{
|
||||
type: "switch",
|
||||
name: "Bypass Local Login",
|
||||
setting: "oauthBypassLocalLogin",
|
||||
description:
|
||||
"Skips the local login page and redirects to the OAuth provider, this only works with one provider enabled.",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Login Only",
|
||||
setting: "oauthLoginOnly",
|
||||
description:
|
||||
"Disables registration and only allows login with OAuth, existing users can link providers for example.",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
@ -878,6 +975,8 @@ export const settings: Array<Setting> = [
|
|||
name: "Discord Redirect URI",
|
||||
setting: "oauthDiscordRedirectUri",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -904,6 +1003,8 @@ export const settings: Array<Setting> = [
|
|||
name: "Google Redirect URI",
|
||||
setting: "oauthGoogleRedirectUri",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -930,6 +1031,8 @@ export const settings: Array<Setting> = [
|
|||
name: "GitHub Redirect URI",
|
||||
setting: "oauthGithubRedirectUri",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -974,6 +1077,8 @@ export const settings: Array<Setting> = [
|
|||
name: "OIDC Redirect URL",
|
||||
setting: "oauthOidcRedirectUri",
|
||||
keyboardType: "default",
|
||||
description:
|
||||
"The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -986,12 +1091,14 @@ export const settings: Array<Setting> = [
|
|||
{
|
||||
type: "category",
|
||||
name: "PWA",
|
||||
description: "Refresh the page after enabling PWA to see any changes.",
|
||||
category: "pwaCategory",
|
||||
children: [
|
||||
{
|
||||
type: "switch",
|
||||
name: "PWA Enabled",
|
||||
setting: "pwaEnabled",
|
||||
description: "Allow users to install the Zipline PWA on their devices.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -999,6 +1106,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "pwaTitle",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline",
|
||||
description: "The title for the PWA",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1006,6 +1114,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "pwaShortName",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline",
|
||||
description: "The short name for the PWA",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1013,6 +1122,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "pwaDescription",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline",
|
||||
description: "The description for the PWA",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1020,11 +1130,13 @@ export const settings: Array<Setting> = [
|
|||
setting: "pwaThemeColor",
|
||||
keyboardType: "default",
|
||||
placeholder: "#000000",
|
||||
description: "The theme color for the PWA",
|
||||
},
|
||||
{
|
||||
type: "colorPicker",
|
||||
name: "Background Color",
|
||||
setting: "pwaBackgroundColor",
|
||||
description: "The background color for the PWA",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -1043,6 +1155,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "httpWebhookOnUpload",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://example.com/upload",
|
||||
description:
|
||||
"The URL to send a POST request to when a file is uploaded.",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1050,6 +1164,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "httpWebhookOnShorten",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://example.com/shorten",
|
||||
description:
|
||||
"The URL to send a POST request to when a URL is shortened.",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -1068,6 +1184,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordWebhookUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://discord.com/api/webhooks/...",
|
||||
description: "The Discord webhook URL to send notifications to",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1075,6 +1192,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordUsername",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline",
|
||||
description: "The username to send notifications as",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1082,6 +1200,7 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordAvatarUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://example.com/avatar.png",
|
||||
description: "The avatar for the webhook",
|
||||
},
|
||||
{
|
||||
type: "save",
|
||||
|
@ -1098,6 +1217,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnUploadWebhookUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://discord.com/api/webhooks/...",
|
||||
description:
|
||||
"The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1105,6 +1226,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnUploadUsername",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline Uploads",
|
||||
description:
|
||||
"The username to send notifications as. If this is left blank, the main username will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1112,6 +1235,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnUploadAvatarUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://example.com/uploadAvatar.png",
|
||||
description:
|
||||
"The avatar for the webhook. If this is left blank, the main avatar will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1119,12 +1244,16 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnUploadContent",
|
||||
keyboardType: "default",
|
||||
multiline: true,
|
||||
description:
|
||||
"The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Embed",
|
||||
setting: "discordOnUploadEmbed",
|
||||
setType: "upload",
|
||||
description:
|
||||
"Send the notification as an embed. This will allow for more customization below.",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
@ -1137,43 +1266,53 @@ export const settings: Array<Setting> = [
|
|||
name: "Title",
|
||||
setting: "discordOnUploadEmbed.title",
|
||||
keyboardType: "default",
|
||||
description: "The title of the embed",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Description",
|
||||
setting: "discordOnUploadEmbed.description",
|
||||
keyboardType: "default",
|
||||
description: "The description of the embed",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Footer",
|
||||
setting: "discordOnUploadEmbed.footer",
|
||||
keyboardType: "default",
|
||||
description: "The footer of the embed",
|
||||
},
|
||||
{
|
||||
type: "colorPicker",
|
||||
name: "Color",
|
||||
setting: "discordOnUploadEmbed.color",
|
||||
description: "The color of the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Thumbnail",
|
||||
setting: "discordOnUploadEmbed.thumbnail",
|
||||
description:
|
||||
"Show the thumbnail (it will show the file if it's an image) in the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Image/Video",
|
||||
setting: "discordOnUploadEmbed.imageOrVideo",
|
||||
description: "Show the image or video in the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Timestamp",
|
||||
setting: "discordOnUploadEmbed.timestamp",
|
||||
description: "Show the timestamp in the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "URL",
|
||||
setting: "discordOnUploadEmbed.url",
|
||||
description:
|
||||
"Makes the title clickable and links to the URL of the file",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1194,6 +1333,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnShortenWebhookUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://discord.com/api/webhooks/...",
|
||||
description:
|
||||
"The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1201,6 +1342,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnShortenUsername",
|
||||
keyboardType: "default",
|
||||
placeholder: "Zipline Shortens",
|
||||
description:
|
||||
"The username to send notifications as. If this is left blank, the main username will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1208,6 +1351,8 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnShortenAvatarUrl",
|
||||
keyboardType: "url",
|
||||
placeholder: "https://example.com/shortenAvatar.png",
|
||||
description:
|
||||
"The avatar for the webhook. If this is left blank, the main avatar will be used",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
|
@ -1215,11 +1360,15 @@ export const settings: Array<Setting> = [
|
|||
setting: "discordOnShortenContent",
|
||||
keyboardType: "default",
|
||||
multiline: true,
|
||||
description:
|
||||
"The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Embed",
|
||||
setting: "discordOnShortenEmbed",
|
||||
description:
|
||||
"Send the notification as an embed. This will allow for more customization below.",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
@ -1232,33 +1381,40 @@ export const settings: Array<Setting> = [
|
|||
name: "Title",
|
||||
setting: "discordOnShortenEmbed.title",
|
||||
keyboardType: "default",
|
||||
description: "The title of the embed",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Description",
|
||||
setting: "discordOnShortenEmbed.description",
|
||||
keyboardType: "default",
|
||||
description: "The description of the embed",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "Footer",
|
||||
setting: "discordOnShortenEmbed.footer",
|
||||
keyboardType: "default",
|
||||
description: "The footer of the embed",
|
||||
},
|
||||
{
|
||||
type: "colorPicker",
|
||||
name: "Color",
|
||||
setting: "discordOnShortenEmbed.color",
|
||||
description: "The color of the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "Timestamp",
|
||||
setting: "discordOnShortenEmbed.timestamp",
|
||||
description: "Show the timestamp in the embed",
|
||||
},
|
||||
{
|
||||
type: "switch",
|
||||
name: "URL",
|
||||
setting: "discordOnShortenEmbed.url",
|
||||
description:
|
||||
"Makes the title clickable and links to the URL of the file",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -154,3 +154,6 @@ export function getRippleColor(color: string, fraction = 0.4) {
|
|||
|
||||
return rgbaToHex(newRed, newGreen, newBlue);
|
||||
}
|
||||
export function getMetricsDifference(first: number, last: number): number {
|
||||
return Math.round(((first - last) / last) * 100) || 0;
|
||||
}
|
||||
|
|
|
@ -253,13 +253,15 @@ export async function uploadFiles(
|
|||
const token = db.get("token");
|
||||
const url = db.get("url");
|
||||
|
||||
if (!url || !token) return "Invalid token or URL";
|
||||
if (!url) return "Invalid URL";
|
||||
|
||||
if (!options.folder && !token) return "Invalid token";
|
||||
|
||||
const headers: {
|
||||
[key: string]: string;
|
||||
} = {};
|
||||
|
||||
headers.Authorization = token;
|
||||
if (token) headers.Authorization = token;
|
||||
headers["X-Zipline-Format"] = options.format?.toLowerCase() || "random";
|
||||
|
||||
if (options.compression)
|
||||
|
|
|
@ -80,8 +80,6 @@ export async function getUserStats(): Promise<APIUserStats | string> {
|
|||
}
|
||||
}
|
||||
|
||||
export function getChartFiles(stats: APIStats) {}
|
||||
|
||||
export function filterStats(data: APIStats, amount = 100) {
|
||||
data.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { APIURLs, APIURL } from "@/types/zipline";
|
||||
import type { APIURLs, APIURL, APIShortenResponse } from "@/types/zipline";
|
||||
import axios, { type AxiosError } from "axios";
|
||||
import * as db from "@/functions/database";
|
||||
|
||||
|
@ -91,12 +91,7 @@ export async function createURL({
|
|||
maxViews,
|
||||
password,
|
||||
enabled = true,
|
||||
}: CreateURLParams): Promise<
|
||||
| {
|
||||
url: string;
|
||||
}
|
||||
| string
|
||||
> {
|
||||
}: CreateURLParams): Promise<APIShortenResponse | string> {
|
||||
const token = db.get("token");
|
||||
const url = db.get("url");
|
||||
|
||||
|
@ -123,7 +118,7 @@ export async function createURL({
|
|||
},
|
||||
);
|
||||
|
||||
return res.data;
|
||||
return res.data as APIShortenResponse;
|
||||
} catch (e) {
|
||||
const error = e as AxiosError;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "zipline",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"start:dev": "APP_VARIANT=development EDGE_PATH=/home/stef/.local/bin/google-chrome expo start --dev-client",
|
||||
|
|
|
@ -22,7 +22,7 @@ export const styles = StyleSheet.create({
|
|||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginVertical: 5
|
||||
marginVertical: 5,
|
||||
},
|
||||
headerText: {
|
||||
marginTop: 5,
|
||||
|
@ -32,6 +32,10 @@ export const styles = StyleSheet.create({
|
|||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
headerDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginLeft: 10,
|
||||
},
|
||||
errorText: {
|
||||
color: "red",
|
||||
fontWeight: "bold",
|
||||
|
@ -68,15 +72,18 @@ export const styles = StyleSheet.create({
|
|||
},
|
||||
externalLinkTitle: {
|
||||
marginTop: 10,
|
||||
marginBottom: 5,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
externalLinkDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginBottom: 5,
|
||||
},
|
||||
popupContent: {
|
||||
height: "auto",
|
||||
width: "100%",
|
||||
zIndex: 99999999999
|
||||
zIndex: 99999999999,
|
||||
},
|
||||
mainHeaderText: {
|
||||
fontSize: 22,
|
||||
|
|
|
@ -3,11 +3,14 @@ import { StyleSheet } from "react-native";
|
|||
export const styles = StyleSheet.create({
|
||||
inputHeader: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
inputDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginBottom: 5,
|
||||
},
|
||||
inputHeaderDisabled: {
|
||||
color: "gray",
|
||||
},
|
||||
|
|
|
@ -66,11 +66,14 @@ export const styles = StyleSheet.create({
|
|||
},
|
||||
inputHeader: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
inputDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginBottom: 5,
|
||||
},
|
||||
inputHeaderDisabled: {
|
||||
color: "gray",
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ export const styles = StyleSheet.create({
|
|||
switchContainer: {
|
||||
flexDirection: "row",
|
||||
marginTop: 10,
|
||||
width: "85%",
|
||||
},
|
||||
switchText: {
|
||||
color: "white",
|
||||
|
@ -12,6 +13,10 @@ export const styles = StyleSheet.create({
|
|||
fontWeight: "bold",
|
||||
textAlignVertical: "center",
|
||||
},
|
||||
switchDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginLeft: 5,
|
||||
},
|
||||
switchTextDisabled: {
|
||||
color: "gray",
|
||||
},
|
||||
|
|
|
@ -19,11 +19,14 @@ export const styles = StyleSheet.create({
|
|||
},
|
||||
inputHeader: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
inputDescription: {
|
||||
color: "#6c7a8d",
|
||||
marginBottom: 5,
|
||||
},
|
||||
textInputDisabled: {
|
||||
color: "gray",
|
||||
},
|
||||
|
|
112
styles/folders/upload.ts
Normal file
112
styles/folders/upload.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
mainContainer: {
|
||||
backgroundColor: "#0c101c",
|
||||
flex: 1,
|
||||
},
|
||||
headerText: {
|
||||
marginTop: 5,
|
||||
marginLeft: 10,
|
||||
fontSize: 23,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
subHeaderText: {
|
||||
marginLeft: 10,
|
||||
fontSize: 13,
|
||||
fontWeight: "bold",
|
||||
color: "gray",
|
||||
},
|
||||
scrollView: {
|
||||
marginTop: 10,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 2,
|
||||
borderColor: "#222c47",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 15,
|
||||
height: 220,
|
||||
},
|
||||
scrollViewKeyboardOpen: {
|
||||
display: "none",
|
||||
},
|
||||
recentFileContainer: {
|
||||
marginHorizontal: 10,
|
||||
marginVertical: 7.5,
|
||||
},
|
||||
inputHeader: {
|
||||
marginTop: 5,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
inputHeaderDisabled: {
|
||||
color: "gray",
|
||||
},
|
||||
inputsContainer: {
|
||||
width: "90%",
|
||||
marginHorizontal: "auto",
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
uploadedFileContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
uploadedFileUrl: {
|
||||
textAlignVertical: "center",
|
||||
color: "#575db5",
|
||||
width: "70%",
|
||||
},
|
||||
failedFileText: {
|
||||
textAlign: "center",
|
||||
color: "white",
|
||||
},
|
||||
popupScrollView: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
maxHeight: 400,
|
||||
},
|
||||
popupSubHeaderText: {
|
||||
marginTop: 10,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerButtons: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
paddingTop: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
errorText: {
|
||||
color: "red",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
},
|
||||
selectText: {
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
fontSize: 15,
|
||||
color: "white",
|
||||
},
|
||||
selectButtonContainer: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
selectItemContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
},
|
||||
folderStatusText: {
|
||||
color: "#6c7a8d",
|
||||
marginTop: 10,
|
||||
marginHorizontal: 20,
|
||||
textAlign: "center",
|
||||
},
|
||||
folderStatusLink: {
|
||||
color: "#575db5",
|
||||
},
|
||||
});
|
|
@ -28,7 +28,7 @@ export const styles = StyleSheet.create({
|
|||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10
|
||||
marginBottom: 10,
|
||||
},
|
||||
dateRangeText: {
|
||||
color: "gray",
|
||||
|
@ -42,11 +42,7 @@ export const styles = StyleSheet.create({
|
|||
borderColor: "#222c47",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 15,
|
||||
padding: 15,
|
||||
marginBottom: 10
|
||||
},
|
||||
statsContainer: {
|
||||
paddingHorizontal: 7,
|
||||
marginBottom: 10,
|
||||
},
|
||||
subHeaderText: {
|
||||
fontSize: 18,
|
||||
|
@ -66,6 +62,21 @@ export const styles = StyleSheet.create({
|
|||
borderRadius: 10,
|
||||
padding: 10,
|
||||
marginHorizontal: 4,
|
||||
marginVertical: 10,
|
||||
},
|
||||
statContainerData: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
statDifferenceContainer: {
|
||||
margin: 10,
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
padding: 4,
|
||||
borderRadius: 8,
|
||||
flexDirection: "row",
|
||||
},
|
||||
statDifferenceText: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
chartContainer: {
|
||||
margin: 10,
|
||||
|
|
|
@ -34,6 +34,10 @@ export const styles = StyleSheet.create({
|
|||
fontWeight: "bold",
|
||||
color: "white",
|
||||
},
|
||||
headerDescription: {
|
||||
marginLeft: 10,
|
||||
color: "#6c7a8d",
|
||||
},
|
||||
errorText: {
|
||||
color: "red",
|
||||
fontWeight: "bold",
|
||||
|
|
|
@ -144,8 +144,23 @@ export interface APIUploadFile {
|
|||
|
||||
export interface APIUploadResponse {
|
||||
files: Array<APIUploadFile>;
|
||||
partialSuccess: boolean;
|
||||
assumedMimetypes: Array<string | null | APIUploadFile>;
|
||||
partialSuccess?: boolean;
|
||||
assumedMimetypes: Array<boolean>;
|
||||
deletesAt?: string;
|
||||
}
|
||||
|
||||
export interface APIShortenResponse {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
code: string;
|
||||
vanity: string | null;
|
||||
destination: string;
|
||||
views: number;
|
||||
maxViews: number | null;
|
||||
enabled: true;
|
||||
userId: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface APITransactionResult {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue