feat: add api files caching, update audioplayer ui

This commit is contained in:
2024-03-25 06:59:40 +07:00
parent 33c91ac0a7
commit 339969baa8
16 changed files with 320 additions and 140 deletions
+1
View File
@@ -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";
+1
View File
@@ -114,6 +114,7 @@ const FilesPage = () => {
const fileType = getFileType(file.path);
if (fileType === "audio") {
audioPlayer.expand();
return audioPlayer.play(data, idx);
}
+20
View File
@@ -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();
+48 -17
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -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;
};