display date
This commit is contained in:
@@ -18,7 +18,7 @@ func Load() *Config {
|
|||||||
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
|
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
|
||||||
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
|
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
|
||||||
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
|
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
|
||||||
ServerHost: envOr("SERVER_HOST", "http://localhost:8080"),
|
ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,45 @@ func (q *Queries) ListFiles(ctx context.Context) ([]File, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listFilesByID = `-- name: ListFilesByID :many
|
||||||
|
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at FROM files
|
||||||
|
WHERE id IN (SELECT value FROM json_each(?))
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListFilesByID(ctx context.Context, jsonEach interface{}) ([]File, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listFilesByID, jsonEach)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []File
|
||||||
|
for rows.Next() {
|
||||||
|
var i File
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.Size,
|
||||||
|
&i.StorageKey,
|
||||||
|
&i.Checksum,
|
||||||
|
&i.OcrText,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const updateFile = `-- name: UpdateFile :exec
|
const updateFile = `-- name: UpdateFile :exec
|
||||||
UPDATE files
|
UPDATE files
|
||||||
SET name = ?, mime_type = ?, ocr_text = ?, updated_at = CURRENT_TIMESTAMP
|
SET name = ?, mime_type = ?, ocr_text = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ WHERE id = ? LIMIT 1;
|
|||||||
SELECT * FROM files
|
SELECT * FROM files
|
||||||
ORDER BY created_at DESC;
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: ListFilesByID :many
|
||||||
|
SELECT * FROM files
|
||||||
|
WHERE id IN (SELECT value FROM json_each(?))
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
-- name: CreateFile :one
|
-- name: CreateFile :one
|
||||||
INSERT INTO files (id, name, mime_type, size, storage_key, checksum, created_at, updated_at)
|
INSERT INTO files (id, name, mime_type, size, storage_key, checksum, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
|||||||
@@ -65,10 +65,15 @@ func (h *FileHandler) List(c *gin.Context) {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := make([]fileResponse, len(files))
|
resp := make([]fileResponse, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|
||||||
|
metadata, err := h.files.Get(f.ID)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
resp[i] = fileResponse{
|
resp[i] = fileResponse{
|
||||||
ID: f.ID,
|
ID: f.ID,
|
||||||
URL: h.urls.GenerateDownloadURL(f.ID),
|
URL: h.urls.GenerateDownloadURL(f.ID),
|
||||||
@@ -76,6 +81,18 @@ func (h *FileHandler) List(c *gin.Context) {
|
|||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resp[i] = fileResponse{
|
||||||
|
ID: f.ID,
|
||||||
|
URL: h.urls.GenerateDownloadURL(f.ID),
|
||||||
|
Name: f.Name,
|
||||||
|
Size: f.Size,
|
||||||
|
Tags: []string{},
|
||||||
|
CreatedAt: metadata.CreatedAt,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.Paginated(c, resp, 1, len(resp))
|
api.Paginated(c, resp, 1, len(resp))
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (s *URLService) GenerateDownloadURL(fileUUID string) string {
|
|||||||
sig := s.sign(fileUUID, expires)
|
sig := s.sign(fileUUID, expires)
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s/api/v1/files/%s?expires=%d&sig=%s",
|
"%s/api/v1/files/download/%s?expires=%d&sig=%s",
|
||||||
s.serverHost,
|
s.serverHost,
|
||||||
fileUUID,
|
fileUUID,
|
||||||
expires,
|
expires,
|
||||||
|
|||||||
+35
-13
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image } from 'react-native';
|
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image, ActivityIndicator } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { useFiles } from '../hooks/useFiles';
|
import { useFiles, useFileImage } from '../hooks/useFiles';
|
||||||
import { FileItem } from '../types';
|
import { FileItem } from '../types';
|
||||||
|
|
||||||
const NUM_COLUMNS = 3;
|
const NUM_COLUMNS = 3;
|
||||||
@@ -20,10 +20,18 @@ type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
|||||||
|
|
||||||
type GroupedFiles = Record<string, FileItem[]>;
|
type GroupedFiles = Record<string, FileItem[]>;
|
||||||
|
|
||||||
|
function parseBackendDate(dateStr: string): Date | null {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
|
||||||
|
if (!match) return new Date(dateStr);
|
||||||
|
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]));
|
||||||
|
}
|
||||||
|
|
||||||
function groupByDate(files: FileItem[]): GroupedFiles {
|
function groupByDate(files: FileItem[]): GroupedFiles {
|
||||||
const groups: GroupedFiles = {};
|
const groups: GroupedFiles = {};
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const d = new Date(file.createdAt);
|
const d = parseBackendDate(file.createdAt);
|
||||||
|
if (!d) continue;
|
||||||
const key = d.toLocaleDateString('fr-FR', {
|
const key = d.toLocaleDateString('fr-FR', {
|
||||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||||
});
|
});
|
||||||
@@ -80,16 +88,7 @@ export function HomeScreen() {
|
|||||||
<Text style={styles.sectionTitle}>{formatDateLabel(dateKey)}</Text>
|
<Text style={styles.sectionTitle}>{formatDateLabel(dateKey)}</Text>
|
||||||
<View style={styles.grid}>
|
<View style={styles.grid}>
|
||||||
{groupFiles.map((file) => (
|
{groupFiles.map((file) => (
|
||||||
<View key={file.id} style={styles.gridItem}>
|
<FileGridItem key={file.id} file={file} />
|
||||||
{file.url ? (
|
|
||||||
<Image source={{ uri: file.url }} style={styles.thumb} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.placeholder}>
|
|
||||||
<Text style={styles.placeholderText}>PDF</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -123,6 +122,29 @@ export function HomeScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FileGridItem({ file }: { file: FileItem }) {
|
||||||
|
const { data, isLoading } = useFileImage(file.id);
|
||||||
|
|
||||||
|
const uri = data?.data?.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.gridItem}>
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={styles.placeholder}>
|
||||||
|
<ActivityIndicator size="small" color="#1976D2" />
|
||||||
|
</View>
|
||||||
|
) : uri ? (
|
||||||
|
<Image source={{ uri }} style={styles.thumb} />
|
||||||
|
) : (
|
||||||
|
<View style={styles.placeholder}>
|
||||||
|
<Text style={styles.placeholderText}>PDF</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export function useFile(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFileImage(fileId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['fileImage', fileId],
|
||||||
|
queryFn: () => apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(`${ENDPOINTS.FILE}/${fileId}`),
|
||||||
|
enabled: !!fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useDeleteFile() {
|
export function useDeleteFile() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user