mirror of
https://github.com/khairul169/home-lab.git
synced 2026-10-02 17:13:57 +07:00
feat: add api files caching, update audioplayer ui
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import "./styles.css";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Stack, router, usePathname } from "expo-router";
|
||||
import { QueryClientProvider } from "react-query";
|
||||
|
||||
@@ -114,6 +114,7 @@ const FilesPage = () => {
|
||||
|
||||
const fileType = getFileType(file.path);
|
||||
if (fileType === "audio") {
|
||||
audioPlayer.expand();
|
||||
return audioPlayer.play(data, idx);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #6366f1;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
* {
|
||||
scrollbar-color: #6366f1 #232e33;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
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";
|
||||
import jsmediatags from "jsmediatags/build2/jsmediatags";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useStore } from "zustand";
|
||||
|
||||
@@ -48,29 +50,18 @@ const AudioPlayerProvider = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function loadMediaTags() {
|
||||
const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
|
||||
async function loadMediaTags() {
|
||||
audioPlayerStore.setState({ mediaTags: null });
|
||||
|
||||
tagsReader.read({
|
||||
onSuccess: (result: any) => {
|
||||
const mediaTagsResult = { ...result };
|
||||
try {
|
||||
const url =
|
||||
`${API_BASEURL}/files/id3-tags${fileData.path}?token=` +
|
||||
authStore.getState().token;
|
||||
|
||||
if (result?.tags?.picture) {
|
||||
const { data, format } = result.tags.picture;
|
||||
let base64String = "";
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
base64String += String.fromCharCode(data[i]);
|
||||
}
|
||||
mediaTagsResult.picture = `data:${format};base64,${base64encode(
|
||||
base64String
|
||||
)}`;
|
||||
delete data?.tags?.picture;
|
||||
}
|
||||
|
||||
audioPlayerStore.setState({ mediaTags: mediaTagsResult });
|
||||
},
|
||||
});
|
||||
const res = await fetchAPI(url);
|
||||
const data = await res.json();
|
||||
audioPlayerStore.setState({ mediaTags: data });
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
loadMediaTags();
|
||||
|
||||
@@ -13,6 +13,12 @@ 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";
|
||||
import { fetchAPI } from "@/lib/api";
|
||||
import { MediaTags } from "@/types/mediaTags";
|
||||
|
||||
const AudioPlayer = () => {
|
||||
const expanded = useStore(audioPlayerStore, (i) => i.expanded);
|
||||
@@ -41,7 +47,7 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
<Box className="flex-1 items-stretch relative">
|
||||
<Box className="absolute -inset-10 -z-[1]">
|
||||
<Image
|
||||
source={mediaTags?.picture ? { uri: mediaTags?.picture } : bgImage}
|
||||
source={mediaTags?.image ? { uri: mediaTags.image } : bgImage}
|
||||
style={cn("absolute -inset-5 w-full h-full")}
|
||||
resizeMode="cover"
|
||||
blurRadius={10}
|
||||
@@ -58,11 +64,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
onPress={onClose}
|
||||
/>
|
||||
|
||||
<Box className="flex flex-col items-center justify-center p-8 flex-1 overflow-hidden">
|
||||
{mediaTags?.picture ? (
|
||||
<Box className="flex flex-col items-center justify-center p-8 pt-20 flex-1 overflow-hidden">
|
||||
{mediaTags?.image ? (
|
||||
<Box className="aspect-square flex-1 max-h-[400px] mb-8">
|
||||
<Image
|
||||
source={{ uri: mediaTags.picture }}
|
||||
source={{ uri: mediaTags.image }}
|
||||
style={cn("w-full h-full")}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
@@ -74,11 +80,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
className="text-white text-xl md:text-3xl mt-4"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{mediaTags?.tags?.title || filename}
|
||||
{mediaTags?.title || filename}
|
||||
</Text>
|
||||
{mediaTags?.tags?.artist ? (
|
||||
{mediaTags?.artist ? (
|
||||
<Text className="text-white mt-2" numberOfLines={1}>
|
||||
{mediaTags.tags.artist}
|
||||
{mediaTags.artist}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
@@ -157,9 +163,9 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const PLAYLIST_ITEM_HEIGHT = 49;
|
||||
const PLAYLIST_ITEM_HEIGHT = 82;
|
||||
|
||||
const Playlist = () => {
|
||||
const Playlist = React.memo(() => {
|
||||
const playlist = useStore(audioPlayerStore, (i) => i.playlist);
|
||||
const currentIdx = useStore(audioPlayerStore, (i) => i.currentIdx);
|
||||
const containerRef = useRef<any>();
|
||||
@@ -204,9 +210,8 @@ const Playlist = () => {
|
||||
|
||||
<FlatList
|
||||
ref={containerRef}
|
||||
contentContainerStyle={cn("px-4")}
|
||||
data={list}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(i) => i.path}
|
||||
renderItem={({ item }) => (
|
||||
<PlaylistItem
|
||||
@@ -221,7 +226,7 @@ const Playlist = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
type PlaylistItemProps = {
|
||||
file: FileItem;
|
||||
@@ -230,17 +235,43 @@ type PlaylistItemProps = {
|
||||
};
|
||||
|
||||
const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
|
||||
const url =
|
||||
`${API_BASEURL}/files/id3-tags${file.path}?token=` +
|
||||
authStore.getState().token;
|
||||
const { data: id3Tags } = useQuery<MediaTags>({
|
||||
queryKey: ["id3-tags", file.path],
|
||||
queryFn: () => fetchAPI(url).then((i) => i.json()),
|
||||
});
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={cn(
|
||||
"py-4 px-5 border-b border-white/10",
|
||||
isCurrent && "bg-[#323232]"
|
||||
"mb-4 flex flex-row items-center gap-4 border border-transparent",
|
||||
isCurrent && "bg-white/10 border-white/20 rounded-lg overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<Text className="text-white" numberOfLines={1}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Box className="bg-gray-800 w-16 h-16 flex items-center justify-center">
|
||||
{id3Tags?.image ? (
|
||||
<Image
|
||||
source={{ uri: id3Tags.image }}
|
||||
style={cn("w-full h-full")}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<Ionicons name="musical-notes" style={cn("text-white text-2xl")} />
|
||||
)}
|
||||
</Box>
|
||||
<Box className="py-2 flex-1">
|
||||
<Text className="text-white font-bold" numberOfLines={1}>
|
||||
{id3Tags?.title || file.name}
|
||||
</Text>
|
||||
{id3Tags?.artist ? (
|
||||
<Text className="text-white mt-2" numberOfLines={1}>
|
||||
{id3Tags?.artist}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-13
@@ -33,22 +33,14 @@ export class ApiError extends Error {
|
||||
|
||||
async function fetchHandler(input: any, init?: RequestInit) {
|
||||
const token = authStore.getState().token;
|
||||
const config = typeof input === "object" ? input : init || {};
|
||||
|
||||
if (init) {
|
||||
init.headers = new Headers(init.headers);
|
||||
if (token) {
|
||||
init.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
config.headers = new Headers(config.headers);
|
||||
if (token) {
|
||||
config.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (typeof input === "object") {
|
||||
input.headers = new Headers(init.headers);
|
||||
if (token) {
|
||||
input.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(input, init);
|
||||
const res = await fetch(input, config);
|
||||
await checkResponse(res);
|
||||
|
||||
return res;
|
||||
@@ -84,5 +76,6 @@ export {
|
||||
InferResponseType,
|
||||
ClientRequestOptions,
|
||||
ClientResponse,
|
||||
fetchHandler as fetchAPI,
|
||||
};
|
||||
export default api;
|
||||
|
||||
@@ -101,6 +101,10 @@ const seek = async (progress: number) => {
|
||||
sound.setPositionAsync(pos);
|
||||
};
|
||||
|
||||
const expand = () => {
|
||||
audioPlayerStore.setState({ expanded: true });
|
||||
};
|
||||
|
||||
export const audioPlayer = {
|
||||
store: audioPlayerStore,
|
||||
play,
|
||||
@@ -108,4 +112,5 @@ export const audioPlayer = {
|
||||
prev,
|
||||
next,
|
||||
seek,
|
||||
expand,
|
||||
};
|
||||
|
||||
+10
-75
@@ -1,83 +1,18 @@
|
||||
export type MediaTags = {
|
||||
type: string;
|
||||
version: string;
|
||||
major: number;
|
||||
revision: number;
|
||||
flags: Flags;
|
||||
size: number;
|
||||
tags: Tags;
|
||||
picture?: string;
|
||||
};
|
||||
|
||||
export type Flags = {
|
||||
unsynchronisation: boolean;
|
||||
extended_header: boolean;
|
||||
experimental_indicator: boolean;
|
||||
footer_present: boolean;
|
||||
};
|
||||
|
||||
export type Tags = {
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
year: string;
|
||||
comment: Comment;
|
||||
track: string;
|
||||
genre: string;
|
||||
// picture: Picture;
|
||||
TALB: Talb;
|
||||
TPE1: Talb;
|
||||
COMM: Comm;
|
||||
TCON: Talb;
|
||||
TIT2: Talb;
|
||||
TRCK: Talb;
|
||||
TYER: Talb;
|
||||
TXXX: Txxx;
|
||||
APIC: APIC;
|
||||
year: string;
|
||||
trackNumber: string;
|
||||
partOfSet: string;
|
||||
artist: string;
|
||||
performerInfo: string;
|
||||
composer: string;
|
||||
userDefinedText: UserDefinedText[];
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type APIC = {
|
||||
id: string;
|
||||
size: number;
|
||||
export type UserDefinedText = {
|
||||
description: string;
|
||||
data: Picture;
|
||||
};
|
||||
|
||||
export type Picture = {
|
||||
format: string;
|
||||
type: string;
|
||||
description: string;
|
||||
data: number[];
|
||||
};
|
||||
|
||||
export type Comm = {
|
||||
id: string;
|
||||
size: number;
|
||||
description: string;
|
||||
data: Comment;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
language: string;
|
||||
short_description: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type Talb = {
|
||||
id: string;
|
||||
size: number;
|
||||
description: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type Txxx = {
|
||||
id: string;
|
||||
size: number;
|
||||
description: string;
|
||||
data: Data;
|
||||
};
|
||||
|
||||
export type Data = {
|
||||
user_description: string;
|
||||
data: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user