diff --git a/backend/routes/files/delete.ts b/backend/routes/files/delete.ts
new file mode 100644
index 0000000..cbb5244
--- /dev/null
+++ b/backend/routes/files/delete.ts
@@ -0,0 +1,22 @@
+import type { Context } from "hono";
+import { z } from "zod";
+import { getFilePath } from "./utils";
+import fs from "fs";
+import { HTTPException } from "hono/http-exception";
+
+const schema = z.object({
+ path: z.string().min(1),
+});
+
+export const deleteFile = async (c: Context) => {
+ const data = schema.parse(await c.req.json());
+ const { path } = getFilePath(data.path);
+
+ if (!fs.existsSync(path)) {
+ throw new HTTPException(404, { message: "File not found!" });
+ }
+
+ await fs.promises.unlink(path);
+
+ return c.json({ result: true });
+};
diff --git a/backend/routes/files/index.ts b/backend/routes/files/index.ts
index 24c2b4a..c96f0ac 100644
--- a/backend/routes/files/index.ts
+++ b/backend/routes/files/index.ts
@@ -6,6 +6,7 @@ import { download } from "./download";
import { getYtdl, ytdl } from "./ytdl";
import cache from "../../middlewares/cache";
import { getId3Tags, getId3Image } from "./id3Tags";
+import { deleteFile } from "./delete";
const cacheFile = cache({ ttl: 86400 });
@@ -16,6 +17,7 @@ const route = new Hono()
.get("/ytdl/:id", getYtdl)
.get("/download/*", cacheFile, download)
.get("/id3-tags/*", cacheFile, getId3Tags)
- .get("/id3-img/*", cacheFile, getId3Image);
+ .get("/id3-img/*", cacheFile, getId3Image)
+ .delete("/delete", deleteFile);
export default route;
diff --git a/src/app/apps/files.tsx b/src/app/apps/files.tsx
index 2ca94e5..dbb9995 100644
--- a/src/app/apps/files.tsx
+++ b/src/app/apps/files.tsx
@@ -5,7 +5,7 @@ import { useAuth } from "@/stores/authStore";
import BackButton from "@ui/BackButton";
import Input from "@ui/Input";
import { Stack } from "expo-router";
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import { useMutation, useQuery } from "react-query";
import FileDrop from "@/components/pages/files/FileDrop";
import { showToast } from "@/stores/toastStore";
@@ -28,6 +28,9 @@ const FilesPage = () => {
path: "",
});
const [viewFile, setViewFile] = useState(null);
+ const [isSearching, setSearching] = useState(false);
+ const [search, setSearch] = useState("");
+
const parentPath =
params.path.length > 0
? params.path.split("/").slice(0, -1).join("/")
@@ -65,6 +68,18 @@ const FilesPage = () => {
}
};
+ const files = useMemo(() => {
+ let items = [...(data || [])].map((item, idx) => ({ ...item, idx }));
+
+ if (search) {
+ items = items.filter((item) =>
+ item.name.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ return items;
+ }, [data, search]);
+
if (!isLoggedIn) {
return null;
}
@@ -81,7 +96,34 @@ const FilesPage = () => {
>
, title: "Files" }}
+ options={{
+ headerLeft: () => ,
+ title: "Files",
+ headerRight: () => (
+ }
+ onPress={() => {
+ setSearching(!isSearching);
+ setSearch("");
+ }}
+ />
+ ),
+ headerTitleAlign: isSearching ? "center" : undefined,
+ headerTitle: isSearching
+ ? () => (
+ }
+ />
+ )
+ : undefined,
+ }}
/>
@@ -106,8 +148,8 @@ const FilesPage = () => {
{
+ files={files}
+ onSelect={(file: FileItem & { idx: number }) => {
if (file.isDirectory) {
return setParams({ path: file.path });
}
@@ -115,7 +157,7 @@ const FilesPage = () => {
const fileType = getFileType(file.path);
if (fileType === "audio") {
audioPlayer.expand();
- return audioPlayer.play(data, idx);
+ return audioPlayer.play(data, file.idx);
}
setViewFile(file);
diff --git a/src/components/containers/AudioPlayerProvider.tsx b/src/components/containers/AudioPlayerProvider.tsx
index a00341f..d187495 100644
--- a/src/components/containers/AudioPlayerProvider.tsx
+++ b/src/components/containers/AudioPlayerProvider.tsx
@@ -1,7 +1,6 @@
import { getFileUrl } from "@/app/apps/lib";
import { fetchAPI } from "@/lib/api";
import { API_BASEURL } from "@/lib/constants";
-import { base64encode } from "@/lib/utils";
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
import authStore from "@/stores/authStore";
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
@@ -9,9 +8,10 @@ import { useEffect, useRef } from "react";
import { useStore } from "zustand";
const AudioPlayerProvider = () => {
- const { currentIdx, playlist, repeat, status } = useStore(audioPlayerStore);
-
+ const { currentIdx, playlist, repeat, status, shouldPlay } =
+ useStore(audioPlayerStore);
const soundRef = useRef(null);
+ const lastStatusRef = useRef(new Date());
useEffect(() => {
if (!playlist?.length || currentIdx < 0) {
@@ -27,8 +27,16 @@ const AudioPlayerProvider = () => {
soundRef.current = sound;
audioPlayerStore.setState({ sound });
+ sound.setProgressUpdateIntervalAsync(1000);
sound.setIsLoopingAsync(repeat);
sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => {
+ const curDate = new Date();
+ const diff = curDate.getTime() - lastStatusRef.current.getTime();
+ if (diff < 1000) {
+ return;
+ }
+
+ lastStatusRef.current = curDate;
audioPlayerStore.setState({ status: st as any });
if (st.didJustFinish) {
@@ -65,17 +73,24 @@ const AudioPlayerProvider = () => {
}
loadMediaTags();
- play();
+
+ if (shouldPlay) {
+ play();
+ }
return () => {
- soundRef.current?.unloadAsync();
- soundRef.current = null;
+ const sound = soundRef.current;
+ if (sound) {
+ sound.unloadAsync();
+ sound.setOnPlaybackStatusUpdate(null);
+ soundRef.current = null;
+ }
};
- }, [currentIdx, playlist]);
+ }, [currentIdx, playlist, shouldPlay]);
useEffect(() => {
- if (status?.isLoaded) {
- soundRef.current?.setIsLoopingAsync(repeat);
+ if (status?.isLoaded && soundRef.current) {
+ soundRef.current.setIsLoopingAsync(repeat);
}
}, [repeat, status?.isLoaded]);
diff --git a/src/components/pages/files/FileMenu.tsx b/src/components/pages/files/FileMenu.tsx
index 6799e0f..2a7cd7f 100644
--- a/src/components/pages/files/FileMenu.tsx
+++ b/src/components/pages/files/FileMenu.tsx
@@ -8,6 +8,11 @@ import ActionSheet from "@ui/ActionSheet";
import { HStack } from "@ui/Stack";
import Button from "@ui/Button";
import { openFile } from "@/app/apps/lib";
+import { showDialog } from "@/stores/dialogStore";
+import api from "@/lib/api";
+import { useMutation } from "react-query";
+import { useFilesContext } from "./FilesContext";
+import { showToast } from "@/stores/toastStore";
type Store = {
isVisible: boolean;
@@ -26,12 +31,30 @@ export const openFileMenu = (file: FileItem) => {
const FileMenu = () => {
const { isVisible, file } = useStore(store);
const onClose = () => store.setState({ isVisible: false });
+ const { refresh } = useFilesContext();
+
+ const deleteMutation = useMutation({
+ mutationFn: (json: any) => api.files.delete.$delete({ json }),
+ onSuccess: () => {
+ refresh();
+ showToast("File deleted!");
+ },
+ });
const onDownload = () => {
openFile(file, true);
onClose();
};
+ const onDelete = () => {
+ showDialog(
+ "Delete file",
+ "Are you sure you want to delete this file?",
+ () => deleteMutation.mutate({ path: file?.path })
+ );
+ onClose();
+ };
+
return (
@@ -45,7 +68,9 @@ const FileMenu = () => {
} onPress={onDownload}>
Download
- }>Delete
+ } onPress={onDelete}>
+ Delete
+
diff --git a/src/components/pages/music/AudioPlayer.tsx b/src/components/pages/music/AudioPlayer.tsx
index 8b732cc..b0c2cec 100644
--- a/src/components/pages/music/AudioPlayer.tsx
+++ b/src/components/pages/music/AudioPlayer.tsx
@@ -13,7 +13,6 @@ import Input from "@ui/Input";
import { useStore } from "zustand";
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
import Modal from "react-native-modal";
-import { getFileUrl } from "@/app/apps/lib";
import { useQuery } from "react-query";
import { API_BASEURL } from "@/lib/constants";
import authStore from "@/stores/authStore";
diff --git a/src/components/pages/music/MiniPlayer.tsx b/src/components/pages/music/MiniPlayer.tsx
index 4ddee19..3659f48 100644
--- a/src/components/pages/music/MiniPlayer.tsx
+++ b/src/components/pages/music/MiniPlayer.tsx
@@ -6,31 +6,39 @@ import Button from "@ui/Button";
import { Ionicons } from "@ui/Icons";
import { HStack } from "@ui/Stack";
import Text from "@ui/Text";
-import React from "react";
+import React, { useState } from "react";
import { Pressable } from "react-native";
import { useStore } from "zustand";
const MiniPlayer = () => {
const { status, playlist, currentIdx, mediaTags } =
useStore(audioPlayerStore);
+ const [minimize, setMinimize] = useState(true);
const current = playlist[currentIdx];
const filename = getFilename(current?.path);
- if (!status?.isLoaded) {
- return null;
+ const onExpand = () => audioPlayerStore.setState({ expanded: true });
+
+ if (minimize) {
+ return (
+ }
+ className="absolute bottom-4 right-4 rounded-full"
+ size="icon-lg"
+ onPress={() => {
+ setMinimize(false);
+ onExpand();
+ }}
+ />
+ );
}
return (
<>
- audioPlayerStore.setState({ expanded: true })}
- >
-
- {mediaTags?.tags?.title || filename || "..."}
-
+
+ {mediaTags?.title || filename || "..."}
{
icon={}
onPress={audioPlayer.next}
/>
+ }
+ iconClassName="text-xl"
+ onPress={() => setMinimize(true)}
+ />
>
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 9a9d609..23b8764 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -15,12 +15,14 @@ const buttonVariants = cva(
ghost: "",
link: "text-primary underline-offset-4",
outline: "border border-primary",
+ icon: "",
},
size: {
default: "h-10 px-4",
sm: "h-8 px-2",
lg: "h-12 px-8",
icon: "h-10 w-10 px-0",
+ "icon-lg": "h-14 w-14 px-0",
},
},
defaultVariants: {
@@ -39,12 +41,14 @@ const buttonTextVariants = cva("text-center font-medium", {
ghost: "text-primary",
link: "text-primary-foreground underline",
outline: "text-primary",
+ icon: "text-secondary-foreground",
},
size: {
default: "text-base",
sm: "text-sm",
lg: "text-xl",
icon: "text-base",
+ "icon-lg": "text-lg",
},
},
defaultVariants: {
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
index 9c8c0b5..887ec8d 100644
--- a/src/components/ui/Input.tsx
+++ b/src/components/ui/Input.tsx
@@ -10,6 +10,7 @@ type BaseInputProps = ComponentPropsWithClassName & {
label?: string;
inputClassName?: string;
error?: string;
+ leftElement?: React.ReactNode;
};
type InputProps = BaseInputProps & {
@@ -22,20 +23,30 @@ const BaseInput = ({
inputClassName,
label,
error,
+ leftElement,
...props
}: BaseInputProps) => {
return (
{label ? {label} : null}
-
+
+ {leftElement ? (
+
+ {leftElement}
+
+ ) : null}
+
+
+
{error ? (
{error}
diff --git a/src/hooks/useDisclosure.ts b/src/hooks/useDisclosure.ts
new file mode 100644
index 0000000..ce29f97
--- /dev/null
+++ b/src/hooks/useDisclosure.ts
@@ -0,0 +1,17 @@
+import { useState } from "react";
+
+export const useDisclosure = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [data, setData] = useState();
+
+ const open = (data?: any) => {
+ setIsOpen(true);
+ setData(data);
+ };
+
+ const close = () => {
+ setIsOpen(false);
+ };
+
+ return { isOpen, open, close, data };
+};
diff --git a/src/stores/audioPlayerStore.ts b/src/stores/audioPlayerStore.ts
index 8953fbb..c4faada 100644
--- a/src/stores/audioPlayerStore.ts
+++ b/src/stores/audioPlayerStore.ts
@@ -19,6 +19,7 @@ type AudioPlayerStore = {
status: AVPlaybackStatusSuccess | null;
mediaTags: MediaTags | null;
expanded: boolean;
+ shouldPlay: boolean;
};
export const audioPlayerStore = createStore(
@@ -32,6 +33,7 @@ export const audioPlayerStore = createStore(
status: null,
mediaTags: null,
expanded: false,
+ shouldPlay: false,
}),
{
name: "audioPlayer",
@@ -42,6 +44,7 @@ export const audioPlayerStore = createStore(
status: null,
mediaTags: null,
expanded: false,
+ shouldPlay: false,
}),
}
)
@@ -58,6 +61,7 @@ const play = (files: FileItem[], idx: number) => {
currentIdx,
status: null,
mediaTags: null,
+ shouldPlay: true,
});
};
@@ -72,7 +76,14 @@ const advanceBy = (increment: number) => {
};
const togglePlay = async () => {
- const { sound, status } = audioPlayerStore.getState();
+ const { sound, status, shouldPlay } = audioPlayerStore.getState();
+
+ if (!shouldPlay || !sound || !status?.isPlaying) {
+ console.log("shoud play toggle");
+ audioPlayerStore.setState({ shouldPlay: true });
+ return;
+ }
+
if (!sound) {
return;
}