Compare commits

..

2 Commits

Author SHA1 Message Date
loveuer
3ff83f12b4 wip: file preview 2024-10-14 22:31:59 +08:00
zhaoyupeng
6f15f82122 wip: file list 2024-10-14 18:08:49 +08:00
12 changed files with 649 additions and 870 deletions

View File

@ -1,39 +0,0 @@
name: Auto Build Windows
on:
push:
tags:
- 'v*'
jobs:
build-job:
runs-on: windows-latest
permissions:
id-token: write
contents: write
pull-requests: write
repository-projects: write
steps:
- name: install node
uses: actions/checkout@v4
with:
node-version: '20'
- name: install golang
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: install wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: build
run: wails build -ldflags='-s -w'
- name: create release
id: create_release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
title: "Release_Windows_${{ github.ref_name }}"
files: |
build/bin/nf-disk.exe

View File

@ -1,273 +1,229 @@
import {
Button,
Input,
makeStyles,
MenuItem,
MenuList,
mergeClasses,
tokens,
Tooltip,
} from "@fluentui/react-components";
Button,
Input,
makeStyles,
MenuItem,
MenuList,
mergeClasses,
tokens,
Tooltip
} from "@fluentui/react-components"
import {
DatabaseLinkRegular,
DeleteRegular,
DismissRegular,
PlugConnectedRegular,
PlugDisconnectedRegular,
SettingsRegular,
DatabaseLinkRegular,
DeleteRegular,
DismissRegular,
PlugConnectedRegular, PlugDisconnectedRegular,
SettingsRegular
} from "@fluentui/react-icons";
import React, { useEffect, useState } from "react";
import { Connection } from "../../interfaces/connection";
import { useToast } from "../../message";
import { Dial } from "../../api";
import { useStoreConnection } from "../../store/connection";
import { useStoreBucket } from "../../store/bucket";
import React, {useEffect, useState} from "react";
import {Connection} from "../../interfaces/connection";
import {useToast} from "../../message";
import {Dial} from "../../api";
import {useStoreConnection} from "../../store/connection";
import {useStoreBucket} from "../../store/bucket";
const useStyles = makeStyles({
list: {
display: "flex",
flexDirection: "row",
height: "100%",
},
content: {
height: "100%",
width: "25rem",
display: "flex",
flexDirection: "column",
},
filter: {
height: "4rem",
width: "100%",
display: "flex",
alignItems: "center",
},
filter_input: {
width: "100%",
marginLeft: "0.5rem",
marginRight: "0.5rem",
},
ctx_menu: {
position: "absolute",
zIndex: "1000",
width: "15rem",
backgroundColor: tokens.colorNeutralBackground1,
boxShadow: `${tokens.shadow16}`,
paddingTop: "4px",
paddingBottom: "4px",
},
items: {
height: "100%",
width: "100%",
},
items_one: {
marginLeft: "0.5rem",
marginRight: "0.5rem",
"&:hover": {
color: tokens.colorNeutralForeground2BrandPressed,
list: {
display: "flex",
flexDirection: "row",
height: "100%",
},
"&.active": {
color: tokens.colorNeutralForeground2BrandPressed,
fontWeight: "bold",
content: {
height: "100%",
width: "25rem",
display: "flex",
flexDirection: "column",
},
"& > span": {
display: "flex",
filter: {
height: "4rem",
width: "100%",
display: "flex",
alignItems: "center",
},
},
items_disconn: {
marginLeft: "auto",
},
slider: {
height: "100%",
width: "1px",
// todo: resize
// cursor: 'ew-resize',
"& > div": {
height: "100%",
width: "1px",
backgroundColor: "lightgray",
filter_input: {
width: "100%",
marginLeft: "0.5rem",
marginRight: "0.5rem",
},
},
});
ctx_menu: {
position: "absolute",
zIndex: "1000",
width: "15rem",
backgroundColor: tokens.colorNeutralBackground1,
boxShadow: `${tokens.shadow16}`,
paddingTop: "4px",
paddingBottom: "4px",
},
items: {
height: "100%",
width: "100%",
},
items_one: {
marginLeft: "0.5rem",
marginRight: "0.5rem",
"&:hover": {
color: tokens.colorNeutralForeground2BrandPressed,
},
"&.active": {
color: tokens.colorNeutralForeground2BrandPressed,
fontWeight: "bold",
},
"& > span": {
display: "flex",
},
},
items_disconn: {
marginLeft: "auto",
},
slider: {
height: '100%', width: '1px',
// todo: resize
// cursor: 'ew-resize',
'& > div': {
height: '100%', width: '1px',
backgroundColor: 'lightgray',
},
},
})
export function ConnectionList() {
const styles = useStyles();
const { dispatchMessage } = useToast();
const { conn_get, conn_list, conn_set } = useStoreConnection();
const [conn_filter, set_conn_filter] = useState<string>("");
const { bucket_get, bucket_set } = useStoreBucket();
const [ctx_menu, set_ctx_menu] = useState<{
x: number;
y: number;
display: "none" | "block";
}>({ x: 0, y: 0, display: "none" });
const [menu_conn, set_menu_conn] = useState<Connection | null>(null);
const styles = useStyles()
const {dispatchMessage} = useToast();
const {conn_get, conn_list, conn_set} = useStoreConnection();
const [conn_filter, set_conn_filter] = useState<string>('');
const {bucket_get, bucket_set} = useStoreBucket()
const [ctx_menu, set_ctx_menu] = useState<{
x: number,
y: number,
display: 'none' | 'block'
}>({x: 0, y: 0, display: 'none'});
const [menu_conn, set_menu_conn] = useState<Connection | null>(null);
useEffect(() => {
document.addEventListener("click", (e) => {
set_ctx_menu({ x: 0, y: 0, display: "none" });
});
setTimeout(() => {
conn_get().then();
}, 1000);
return () => {
document.removeEventListener("click", (e) => {});
};
}, []);
useEffect(() => {
document.addEventListener("click", (e) => {
set_ctx_menu({x: 0, y: 0, display: 'none'});
})
setTimeout(() => {
conn_get().then()
}, 1000)
return () => {
document.removeEventListener("click", (e) => {
})
}
}, [])
async function handleSelect(item: Connection) {
conn_list.map((one: Connection) => {
if (item.id === one.id && one.active) {
conn_set(one);
bucket_get(one, false);
bucket_set(null);
}
});
}
async function handleConnect(item: Connection | null) {
if (!item) return;
let res = await Dial("/api/connection/connect", { id: item.id });
if (res.status !== 200) {
dispatchMessage(res.msg, "error");
return;
}
await conn_set({ ...item, active: true });
bucket_get(item, true);
await bucket_set(null);
}
async function handleDisconnect(item: Connection | null) {
if (!item) return;
let res = await Dial("/api/connection/disconnect", { id: item.id });
if (res.status !== 200) {
dispatchMessage(res.msg, "error");
return;
}
await conn_set({ ...item, active: false });
}
async function handleRightClick(
e: React.MouseEvent<HTMLDivElement>,
item: Connection
) {
e.preventDefault();
set_menu_conn(item);
const ele = document.querySelector("#list-connection-container");
const eleX = ele ? ele.clientWidth : 0;
const eleY = ele ? ele.clientHeight : 0;
const positionX =
e.pageX + eleX > window.innerWidth ? e.pageX - eleX : e.pageX;
const positionY =
e.pageY + eleY > window.innerHeight ? e.pageY - eleY : e.pageY;
set_ctx_menu({
x: positionX,
y: positionY,
display: "block",
});
}
return (
<div className={styles.list}>
<div className={styles.content}>
<div className={styles.filter}>
<Input
value={conn_filter}
className={styles.filter_input}
contentAfter={
<Button
appearance={"transparent"}
onClick={async () => {
set_conn_filter("");
}}
size="small"
icon={<DismissRegular />}
/>
async function handleSelect(item: Connection) {
conn_list.map((one: Connection) => {
if (item.id === one.id && one.active) {
conn_set(one)
bucket_get(one, false)
bucket_set(null)
}
placeholder="搜索连接"
onChange={(e) => set_conn_filter(e.target.value)}
/>
})
}
async function handleConnect(item: Connection | null) {
if (!item) return;
let res = await Dial('/api/connection/connect', {id: item.id});
if (res.status !== 200) {
dispatchMessage(res.msg, "error")
return
}
await conn_set({...item, active: true})
bucket_get(item, true)
await bucket_set(null)
}
async function handleDisconnect(item: Connection | null) {
if (!item) return;
let res = await Dial('/api/connection/disconnect', {id: item.id})
if (res.status !== 200) {
dispatchMessage(res.msg, "error")
return
}
await conn_set({...item, active: false})
}
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Connection) {
e.preventDefault()
set_menu_conn(item)
const ele = document.querySelector('#list-connection-container')
const eleX = ele ? ele.clientWidth : 0;
const eleY = ele ? ele.clientHeight : 0;
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
set_ctx_menu({
x: positionX,
y: positionY,
display: 'block',
})
}
return (
<div className={styles.list}>
<div className={styles.content}>
<div className={styles.filter}>
<Input
value={conn_filter}
className={styles.filter_input}
contentAfter={
<Button appearance={'transparent'} onClick={async () => {
set_conn_filter('')
}} size="small" icon={<DismissRegular/>}/>
}
placeholder="搜索连接"
onChange={(e) => set_conn_filter(e.target.value)}
/>
</div>
<div
id={'list-connection-container'}
className={styles.ctx_menu}
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
>
<MenuList>
<MenuItem
onClick={async () => {
await handleConnect(menu_conn)
}}
icon={<PlugConnectedRegular/>}></MenuItem>
<MenuItem
onClick={async () => {
await handleDisconnect(menu_conn)
}}
icon={<PlugDisconnectedRegular/>}></MenuItem>
<MenuItem icon={<SettingsRegular/>}></MenuItem>
<MenuItem icon={<DeleteRegular/>}></MenuItem>
</MenuList>
</div>
<div className={styles.items}>
<MenuList>
{conn_list.filter(item => item.name.includes(conn_filter)).map(item => {
return <MenuItem
className={item.active ? mergeClasses(styles.items_one, "active") : styles.items_one}
onClick={async () => {
await handleSelect(item)
}}
onDoubleClick={async () => {
await handleConnect(item)
}}
onContextMenu={async (e) => {
await handleRightClick(e, item)
}}
icon={<DatabaseLinkRegular/>}
key={item.id}>
{item.name}
</MenuItem>
})}
</MenuList>
</div>
</div>
<div className={styles.slider}>
<div></div>
</div>
</div>
<div
id={"list-connection-container"}
className={styles.ctx_menu}
style={{
left: ctx_menu.x,
top: ctx_menu.y,
display: ctx_menu.display,
}}
>
<MenuList>
<MenuItem
onClick={async () => {
await handleConnect(menu_conn);
}}
icon={<PlugConnectedRegular />}
>
</MenuItem>
<MenuItem
onClick={async () => {
await handleDisconnect(menu_conn);
}}
icon={<PlugDisconnectedRegular />}
>
</MenuItem>
<MenuItem
onClick={() => {
dispatchMessage("暂未实现", "warning");
}}
icon={<SettingsRegular />}
>
</MenuItem>
<MenuItem
onClick={() => {
dispatchMessage("暂未实现", "warning");
}}
icon={<DeleteRegular />}
>
</MenuItem>
</MenuList>
</div>
<div className={styles.items}>
<MenuList>
{conn_list
.filter((item) => item.name.includes(conn_filter))
.map((item) => {
return (
<MenuItem
className={
item.active
? mergeClasses(styles.items_one, "active")
: styles.items_one
}
onClick={async () => {
await handleSelect(item);
}}
onDoubleClick={async () => {
await handleConnect(item);
}}
onContextMenu={async (e) => {
await handleRightClick(e, item);
}}
icon={<DatabaseLinkRegular />}
key={item.id}
>
{item.name}
</MenuItem>
);
})}
</MenuList>
</div>
</div>
<div className={styles.slider}>
<div></div>
</div>
</div>
);
}
)
}

View File

@ -1,165 +1,147 @@
import {
DialogTrigger,
DialogSurface,
DialogTitle,
DialogBody,
DialogActions,
DialogContent,
Button,
Spinner,
Field,
Input,
makeStyles,
tokens,
DialogTrigger,
DialogSurface,
DialogTitle,
DialogBody,
DialogActions,
DialogContent,
Button, Spinner, Field, Input, makeStyles, tokens,
} from "@fluentui/react-components";
import { useState } from "react";
import { CheckmarkFilled, DismissRegular } from "@fluentui/react-icons";
import { useToast } from "../../message";
import { Dial } from "../../api";
import { useStoreConnection } from "../../store/connection";
import {useState} from "react";
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
import {useToast} from "../../message";
import {Dial} from "../../api";
import {useStoreConnection} from "../../store/connection";
const useActionStyle = makeStyles({
container: {
backgroundColor: tokens.colorNeutralBackground1,
display: "flex",
flexDirection: "row",
height: "100%",
width: "100%",
gridColumnStart: 0,
},
test: {},
container: {
backgroundColor: tokens.colorNeutralBackground1,
display: "flex",
flexDirection: "row",
height: "100%",
width: "100%",
gridColumnStart: 0,
},
test: {}
});
export interface ConnectionCreateProps {
openFn: (open: boolean) => void;
openFn: (open: boolean) => void;
}
export function ConnectionCreate(props: ConnectionCreateProps) {
const actionStyle = useActionStyle();
const { dispatchMessage } = useToast();
const [testLoading, setTestLoading] = useState<
"initial" | "loading" | "success" | "error"
>("initial");
const { conn_get } = useStoreConnection();
const buttonIcon =
testLoading === "loading" ? (
<Spinner size="tiny" />
) : testLoading === "success" ? (
<CheckmarkFilled />
) : testLoading === "error" ? (
<DismissRegular />
) : null;
const [value, setValue] = useState<{
name: string;
endpoint: string;
access: string;
key: string;
}>({
name: "",
endpoint: "",
access: "",
key: "",
});
const actionStyle = useActionStyle();
const {dispatchMessage} = useToast();
const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
const {conn_get} = useStoreConnection();
const buttonIcon =
testLoading === "loading" ? (
<Spinner size="tiny"/>
) : testLoading === "success" ? (
<CheckmarkFilled/>
) : testLoading === "error" ? (
<DismissRegular/>
) : null;
const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
name: '',
endpoint: '',
access: '',
key: ''
})
async function test() {
setTestLoading("loading");
let res = await Dial<string>("/api/connection/test", value);
const status = res.status === 200 ? "success" : "error";
setTestLoading(status);
dispatchMessage(res.msg, status);
}
async function create() {
let res = await Dial("/api/connection/create", value);
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
if (res.status === 200) {
dispatchMessage("新建连接成功", "success");
await conn_get();
props.openFn(false);
async function test() {
setTestLoading("loading")
let res = await Dial<string>("/api/connection/test", value)
const status = res.status === 200 ? "success" : "error"
setTestLoading(status);
dispatchMessage(res.msg, status)
}
}
return (
<>
<DialogSurface>
<DialogBody>
<DialogTitle>S3连接</DialogTitle>
<DialogContent>
<div className="connection-container">
<div className="connection-form">
<div className="connection-form-field">
<Field label="name">
<Input
placeholder="名称 (example: 测试S3-minio)"
value={value.name}
onChange={(e) => {
setValue({ ...value, name: e.target.value });
}}
/>
</Field>
</div>
<div className="connection-form-field">
<Field label="endpoint" required>
<Input
placeholder="地址 (example: https://ip_or_server-name:port)"
value={value.endpoint}
required
onChange={(e) => {
setValue({ ...value, endpoint: e.target.value });
}}
/>
</Field>
</div>
<div className="connection-form-field">
<Field label="secret access" required>
<Input
placeholder=""
required
value={value.access}
onChange={(e) => {
setValue({ ...value, access: e.target.value });
}}
/>
</Field>
</div>
<div className="connection-form-field">
<Field label="secret key" required>
<Input
placeholder=""
required
value={value.key}
onChange={(e) => {
setValue({ ...value, key: e.target.value });
}}
/>
</Field>
</div>
</div>
</div>
</DialogContent>
<DialogActions className={actionStyle.container}>
<Button
className={actionStyle.test}
appearance="transparent"
icon={buttonIcon}
onClick={async () => await test()}
>
</Button>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary"></Button>
</DialogTrigger>
<Button
onClick={async () => {
await create();
}}
appearance="primary"
>
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
async function create() {
// self
// qUvfW8xpOTc23O96
// eTcuc8BebHPVpZZwIaNmzfwxRxPYGfTj
// 48-dev
// OSIsqPrl0TkAUj3R
// FYF4BBzL2j2ObbVYH0FrvOZqJf1EACRy
let res = await Dial("/api/connection/create", value)
dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
if (res.status === 200) {
dispatchMessage("新建连接成功", "success");
await conn_get()
props.openFn(false)
}
}
return <>
<DialogSurface>
<DialogBody>
<DialogTitle>S3连接</DialogTitle>
<DialogContent>
<div className='connection-container'>
<div className='connection-form'>
<div className='connection-form-field'>
<Field
label="name"
>
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
onChange={(e) => {
setValue({...value, name: e.target.value});
}}/>
</Field>
</div>
<div className='connection-form-field'>
<Field
label="endpoint"
required
>
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
value={value.endpoint}
required
onChange={(e) => {
setValue({...value, endpoint: e.target.value});
}}/>
</Field>
</div>
<div className='connection-form-field'>
<Field
label="secret access"
required
>
<Input placeholder=''
required
value={value.access} onChange={(e) => {
setValue({...value, access: e.target.value});
}}/>
</Field>
</div>
<div className='connection-form-field'>
<Field
label="secret key"
required
>
<Input placeholder=''
required
value={value.key} onChange={(e) => {
setValue({...value, key: e.target.value});
}}/>
</Field>
</div>
</div>
</div>
</DialogContent>
<DialogActions className={actionStyle.container}>
<Button className={actionStyle.test} appearance='transparent' icon={buttonIcon}
onClick={async () => await test()}></Button>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary"></Button>
</DialogTrigger>
<Button onClick={async () => {
await create()
}} appearance="primary"></Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</>
);
}
}

View File

@ -22,14 +22,10 @@ export function Content() {
const [preview, set_preview] = useState<{ url: string, content_type: string }>({url: '', content_type: ''})
const {bucket_active} = useStoreBucket()
const closeFn = () => {
set_preview({url: '', content_type: ''})
}
return <div className={styles.content}>
<Path/>
{
preview.url ? <PreviewFile url={preview.url} content_type={preview.content_type} close={closeFn}/> :
preview.url ? <PreviewFile url={preview.url} content_type={preview.content_type}/> :
(
bucket_active ?
<ListFileComponent set_preview_fn={set_preview}/> : <ListBucketComponent/>

View File

@ -1,340 +1,292 @@
import {makeStyles, MenuItem, MenuList, Spinner, Text, tokens} from "@fluentui/react-components";
import {
makeStyles,
MenuItem,
MenuList,
Spinner,
Text,
tokens,
} from "@fluentui/react-components";
import {
ArrowDownloadFilled,
DeleteRegular,
DocumentBulletListRegular,
DocumentChevronDoubleRegular,
DocumentCssRegular,
DocumentDatabaseRegular,
DocumentDismissRegular,
DocumentImageRegular,
DocumentJavascriptRegular,
DocumentPdfRegular,
DocumentYmlRegular,
FolderRegular,
PreviewLinkRegular,
ArrowDownloadFilled,
DeleteRegular,
DocumentBulletListRegular,
DocumentChevronDoubleRegular,
DocumentCssRegular,
DocumentDatabaseRegular,
DocumentDismissRegular,
DocumentImageRegular,
DocumentJavascriptRegular,
DocumentPdfRegular,
DocumentYmlRegular,
FolderRegular,
PreviewLinkRegular
} from "@fluentui/react-icons";
import { VirtualizerScrollView } from "@fluentui/react-components/unstable";
import React, { useEffect, useState } from "react";
import { useStoreBucket } from "../../store/bucket";
import { S3File } from "../../interfaces/connection";
import { useStoreFile, useStoreFileFilter } from "../../store/file";
import { useStoreConnection } from "../../store/connection";
import { TrimSuffix } from "../../hook/strings";
import { Dial } from "../../api";
import { useToast } from "../../message";
import { CanPreview } from "../../hook/preview";
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
import React, {useEffect, useState} from "react";
import {useStoreBucket} from "../../store/bucket";
import {S3File} from "../../interfaces/connection";
import {useStoreFile, useStoreFileFilter} from "../../store/file";
import {useStoreConnection} from "../../store/connection";
import {TrimSuffix} from "../../hook/strings";
import {Dial} from "../../api";
import {useToast} from "../../message";
import {CanPreview} from "../../hook/preview";
import {useStorePreview} from "../../store/preview";
const useStyles = makeStyles({
container: {
marginTop: "0.5rem",
maxWidth: "calc(100vw - 25.2rem)",
width: "calc(100vw - 25.2rem)",
maxHeight: "calc(100vh - 10rem)",
height: "calc(100vh - 10rem)",
},
loading: {
flex: "1",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
},
no_data: {
flex: "1",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "8rem",
flexDirection: "column",
},
list: {
flex: "1",
height: "100%",
width: "100%",
},
row: {
height: "32px",
lineHeight: "32px",
display: "flex",
marginLeft: "0.5rem",
marginRight: "0.5rem",
},
item: {
width: "100%",
maxWidth: "100%",
"&:hover": {
color: tokens.colorNeutralForeground2BrandPressed,
container: {
marginTop: '0.5rem',
maxWidth: 'calc(100vw - 25.2rem)',
width: 'calc(100vw - 25.2rem)',
maxHeight: 'calc(100vh - 10rem)',
height: 'calc(100vh - 10rem)',
},
},
text: {
overflow: "hidden",
width: "calc(100vw - 32rem)",
display: "block",
},
ctx_menu: {
position: "absolute",
zIndex: "1000",
width: "15rem",
backgroundColor: tokens.colorNeutralBackground1,
boxShadow: `${tokens.shadow16}`,
paddingTop: "4px",
paddingBottom: "4px",
},
});
loading: {
flex: "1",
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
},
no_data: {
flex: "1",
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '8rem',
flexDirection: 'column',
},
list: {
flex: "1",
height: '100%',
width: '100%',
},
row: {
height: '32px',
display: 'flex',
marginLeft: '0.5rem',
marginRight: '0.5rem',
},
item: {
width: '100%',
maxWidth: '100%',
"&:hover": {
color: tokens.colorNeutralForeground2BrandPressed,
}
},
text: {
overflow: 'hidden',
width: 'calc(100vw - 32rem)',
display: "block",
},
ctx_menu: {
position: "absolute",
zIndex: "1000",
width: "15rem",
backgroundColor: tokens.colorNeutralBackground1,
boxShadow: `${tokens.shadow16}`,
paddingTop: "4px",
paddingBottom: "4px",
},
})
export interface ListFileComponentProps {
set_preview_fn: React.Dispatch<
React.SetStateAction<{ url: string; content_type: string }>
>;
set_preview_fn: React.Dispatch<React.SetStateAction<{url:string, content_type: string}>>
}
export function ListFileComponent(props: ListFileComponentProps) {
const styles = useStyles();
const { dispatchMessage } = useToast();
const { conn_active } = useStoreConnection();
const { bucket_active } = useStoreBucket();
const { file_active, files_get, file_set, files_list } = useStoreFile();
const { prefix, filter, prefix_set } = useStoreFileFilter();
const [preview_content_type, set_preview_content_type] = useState("");
const [ctx_menu, set_ctx_menu] = useState<{
x: number;
y: number;
display: "none" | "block";
}>({ x: 0, y: 0, display: "none" });
const [loading, set_loading] = useState(true);
useEffect(() => {
document.addEventListener("click", (e) => {
set_ctx_menu({ x: 0, y: 0, display: "none" });
});
return () => {
document.removeEventListener("click", (e) => {});
};
}, []);
const styles = useStyles();
const {dispatchMessage} = useToast();
const {conn_active} = useStoreConnection();
const {bucket_active} = useStoreBucket()
const {file_active, files_get, file_set, files_list} = useStoreFile()
const {prefix, filter, prefix_set} = useStoreFileFilter()
const [preview_content_type, set_preview_content_type] = useState('')
const {preview_get} = useStorePreview()
const [ctx_menu, set_ctx_menu] = useState<{
x: number,
y: number,
display: 'none' | 'block'
}>({x: 0, y: 0, display: 'none'});
const [loading, set_loading] = useState(true)
useEffect(() => {
set_loading(true);
files_get(conn_active!, bucket_active!, prefix, filter).then(() => {
set_loading(false);
});
}, [conn_active, bucket_active, prefix, filter]);
useEffect(() => {
document.addEventListener("click", (e) => {
set_ctx_menu({x: 0, y: 0, display: 'none'});
})
return () => {
document.removeEventListener("click", (e) => {
})
}
}, [])
const filename = (key: string) => {
let strs = TrimSuffix(key, "/").split("/");
return strs[strs.length - 1];
};
useEffect(() => {
set_loading(true)
files_get(conn_active!, bucket_active!, prefix, filter).then(() => {
set_loading(false)
})
}, [conn_active, bucket_active, prefix, filter]);
async function handleClick(item: S3File) {
if (item.type === 1) {
await prefix_set(item.key);
return;
const filename = (key: string) => {
let strs = TrimSuffix(key, "/").split("/")
return strs[strs.length - 1]
}
}
async function handleRightClick(
e: React.MouseEvent<HTMLDivElement>,
item: S3File
) {
e.preventDefault();
await file_set(item.key);
set_preview_content_type(CanPreview(item.name));
const ele = document.querySelector("#list-file-container");
let eleX = ele ? ele.clientWidth : 0;
let eleY = ele ? ele.clientHeight : 0;
const positionX =
e.pageX + eleX > window.innerWidth ? e.pageX - eleX : e.pageX;
const positionY =
e.pageY + eleY > window.innerHeight ? e.pageY - eleY : e.pageY;
set_ctx_menu({
x: positionX,
y: positionY,
display: "block",
});
}
async function handleDownload(file: string | null) {
if (!file) return;
const res1 = await Dial<{ result: string }>("/runtime/dialog/save", {
default_filename: file,
});
if (res1.status !== 200) {
return;
async function handleClick(item: S3File) {
if (item.type === 1) {
await prefix_set(item.key)
return
}
}
if (!res1.data) return;
const res2 = await Dial("/api/file/download", {
conn_id: conn_active?.id,
bucket: bucket_active?.name,
key: file,
location: res1.data.result,
});
if (res2.status === 200) {
dispatchMessage("保存文件成功", "success");
}
}
async function handlePreview() {
const res = await Dial<{ url: string; method: string }>("/api/file/get", {
conn_id: conn_active?.id,
bucket: bucket_active?.name,
key: file_active ?? "",
});
if (res.status !== 200) {
dispatchMessage("预览失败", "warning");
return;
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: S3File) {
e.preventDefault()
await file_set(item.key)
set_preview_content_type(CanPreview(item.name))
const ele = document.querySelector('#list-file-container')
let eleX = ele ? ele.clientWidth : 0
let eleY = ele ? ele.clientHeight : 0
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
set_ctx_menu({
x: positionX,
y: positionY,
display: 'block',
})
}
props.set_preview_fn({
url: res.data.url,
content_type: preview_content_type,
});
}
return (
<div className={styles.container}>
<div
id={"list-file-container"}
className={styles.ctx_menu}
style={{ left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display }}
>
<MenuList>
<MenuItem
onClick={async () => {
await handleDownload(file_active);
}}
icon={<ArrowDownloadFilled />}
>
</MenuItem>
<MenuItem
disabled={!preview_content_type}
onClick={async () => {
await handlePreview();
}}
icon={<PreviewLinkRegular />}
>
</MenuItem>
<MenuItem
onClick={() => {
dispatchMessage("敬请期待...", "warning");
}}
icon={<DeleteRegular />}
>
</MenuItem>
</MenuList>
</div>
<div
className={styles.loading}
style={{ display: loading ? "flex" : "none" }}
>
<Spinner appearance="primary" label="加载中..." />
</div>
<div
className={styles.no_data}
style={{ display: !loading && !files_list.length ? "flex" : "none" }}
>
<div>
<DocumentDismissRegular />
</div>
<Text size={900}></Text>
</div>
<div
style={{ display: !loading && files_list.length ? "block" : "none" }}
>
<VirtualizerScrollView
numItems={files_list.length}
itemSize={32}
container={{
role: "list",
style: { maxHeight: "calc(100vh - 10rem)" },
}}
async function handleDownload(file: string | null) {
if (!file) return
const res1 = await Dial<{ result: string }>("/runtime/dialog/save", {
default_filename: file,
})
if (res1.status !== 200) {
return
}
if (!res1.data) return
const res2 = await Dial('/api/file/download', {
conn_id: conn_active?.id,
bucket: bucket_active?.name,
key: file,
location: res1.data.result,
})
if (res2.status === 200) {
dispatchMessage('保存文件成功', 'success')
}
}
async function handlePreview() {
const res = await Dial<{url:string, method: string}>('/api/file/get', {
conn_id: conn_active?.id,
bucket: bucket_active?.name,
key: file_active ?? "",
})
if (res.status !== 200) {
dispatchMessage('预览失败', 'warning')
return
}
props.set_preview_fn({url: res.data.url, content_type: preview_content_type})
}
return <div className={styles.container}>
<div
id={'list-file-container'}
className={styles.ctx_menu}
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
>
{(idx) => {
return (
<div
className={styles.row}
key={idx}
onClick={async () => {
await handleClick(files_list[idx]);
}}
onContextMenu={async (e) => {
await handleRightClick(e, files_list[idx]);
}}
>
<MenuList>
<MenuItem
className={styles.item}
icon={
files_list[idx].type ? (
<FolderRegular />
) : (
<FileIcon name={files_list[idx].name} />
)
}
>
<Text truncate wrap={false} className={styles.text}>
{filename(files_list[idx].key)}
</Text>
</MenuItem>
</div>
);
}}
</VirtualizerScrollView>
</div>
onClick={async () => {
await handleDownload(file_active)
}}
icon={<ArrowDownloadFilled/>}></MenuItem>
<MenuItem
disabled={!preview_content_type}
onClick={async () => {
await handlePreview()
}}
icon={<PreviewLinkRegular/>}></MenuItem>
<MenuItem icon={<DeleteRegular/>}></MenuItem>
</MenuList>
</div>
<div className={styles.loading} style={{display: loading ? 'flex' : 'none'}}>
<Spinner appearance="primary" label="加载中..."/>
</div>
<div className={styles.no_data} style={{display: (!loading && !files_list.length) ? 'flex' : 'none'}}>
<div>
<DocumentDismissRegular/>
</div>
<Text size={900}>
</Text>
</div>
<div style={{display: (!loading && files_list.length) ? 'block' : 'none'}}>
<VirtualizerScrollView
numItems={files_list.length}
itemSize={32}
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
>
{(idx) => {
return <div
className={styles.row} key={idx}
onClick={async () => {
await handleClick(files_list[idx])
}}
onContextMenu={async (e) => {
await handleRightClick(e, files_list[idx])
}}>
<MenuItem className={styles.item}
icon={files_list[idx].type ? <FolderRegular/> :
<FileIcon name={files_list[idx].name}/>}>
<Text truncate wrap={false} className={styles.text}>
{filename(files_list[idx].key)}
</Text>
</MenuItem>
</div>
}}
</VirtualizerScrollView>
</div>
</div>
);
}
type FileIconProps = {
name: string;
};
name: string
}
function FileIcon(props: FileIconProps) {
const strings = props.name.split(".");
const suffix = strings[strings.length - 1];
switch (suffix.toLowerCase()) {
case "png":
return <DocumentImageRegular />;
case "jpg":
return <DocumentImageRegular />;
case "jpeg":
return <DocumentImageRegular />;
case "gif":
return <DocumentImageRegular />;
case "db":
return <DocumentDatabaseRegular />;
case "sqlite":
return <DocumentDatabaseRegular />;
case "sqlite3":
return <DocumentDatabaseRegular />;
case "pdf":
return <DocumentPdfRegular />;
case "css":
return <DocumentCssRegular />;
case "js":
return <DocumentJavascriptRegular />;
case "yaml":
return <DocumentYmlRegular />;
case "yml":
return <DocumentYmlRegular />;
case "html":
return <DocumentChevronDoubleRegular />;
case "json":
return <DocumentChevronDoubleRegular />;
case "go":
return <DocumentChevronDoubleRegular />;
default:
return <DocumentBulletListRegular />;
}
}
const strings = props.name.split(".")
const suffix = strings[strings.length - 1]
switch (suffix) {
case "png":
return <DocumentImageRegular/>
case "jpg":
return <DocumentImageRegular/>
case "jpeg":
return <DocumentImageRegular/>
case "gif":
return <DocumentImageRegular/>
case "db":
return <DocumentDatabaseRegular/>
case "sqlite":
return <DocumentDatabaseRegular/>
case "sqlite3":
return <DocumentDatabaseRegular/>
case "pdf":
return <DocumentPdfRegular/>
case "css":
return <DocumentCssRegular/>
case "js":
return <DocumentJavascriptRegular/>
case "yaml":
return <DocumentYmlRegular/>
case "yml":
return <DocumentYmlRegular/>
case "html":
return <DocumentChevronDoubleRegular/>
case "json":
return <DocumentChevronDoubleRegular/>
case "go":
return <DocumentChevronDoubleRegular/>
default:
return <DocumentBulletListRegular/>
}
}

View File

@ -1,81 +1,21 @@
import {Button, makeStyles,tokens, Text } from "@fluentui/react-components";
import { DismissRegular } from "@fluentui/react-icons";
import { useEffect } from "react";
import {CardPreview, makeStyles} from "@fluentui/react-components";
const useStyle = makeStyles({
container: {
position: 'absolute',
left: 0,
top: 0,
width: '100vw',
maxWidth: '100vw',
height: '100vh',
maxHeight: '100vh',
zIndex: 100,
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: tokens.colorNeutralBackground1,
},
header: {
height: '4rem',
width: '100%',
display: 'flex',
alignItems: 'center',
},
header_close_button: {
marginLeft: 'auto',
},
body: {
width: '100%',
maxWidth: '100%',
height: '100%',
maxHeight: '100%',
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems:'center',
},
})
export function PreviewFile(props: { url: string, content_type: string, close: () => void }) {
const styles = useStyle()
const category = props.content_type.split('/')[0]
useEffect(() => {
window.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
props.close()
}
})
return () => {
window.removeEventListener('keyup', () => { })
}
}, [])
switch (category) {
case "image":
return <div className={styles.container}>
<div className={styles.header}>
<Button
size="large"
appearance="transparent"
className={styles.header_close_button}
onClick={() => { props.close() }}>
<DismissRegular />
</Button>
</div>
<div className={styles.body}>
<img src={props.url}/>
</div>
</div>
default:
return <div className={styles.container}>
<Text></Text>
</div>
}
})
export function PreviewFile(props: {url:string, content_type:string}) {
const styles = useStyle()
return <div className={styles.container}>
<CardPreview
>
<img src={props.url} />
</CardPreview>
</div>
}

View File

@ -1,6 +1,6 @@
export function CanPreview(filename: string) {
const fs = filename.split(".")
switch (fs[fs.length - 1].toLowerCase()) {
switch (fs[fs.length - 1]) {
case "jpg":
return "image/jpg"
case "jpeg":

View File

@ -2,12 +2,10 @@ package controller
import (
"context"
"fmt"
"github.com/loveuer/nf-disk/internal/api"
"github.com/loveuer/nf-disk/internal/db"
"github.com/loveuer/nf-disk/internal/manager"
"github.com/loveuer/nf-disk/internal/model"
"github.com/loveuer/nf-disk/internal/opt"
"github.com/loveuer/nf-disk/internal/tool"
"github.com/loveuer/nf-disk/ndh"
"github.com/loveuer/nf/nft/log"
@ -39,8 +37,7 @@ func NewApp(gctx context.Context) *App {
func (a *App) Startup(ctx context.Context) {
log.Info("app startup!!!")
a.ctx = ctx
tool.Must(opt.Init())
tool.Must(db.Init(ctx, fmt.Sprintf("sqlite::%s", opt.ConfigFile)))
tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil)))
tool.Must(model.Init(db.Default.Session()))
tool.Must(manager.Init(ctx))
tool.Must(api.Init(ctx))

View File

@ -1,7 +1,9 @@
package model
import (
"github.com/loveuer/nf-disk/internal/opt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func Init(tx *gorm.DB) (err error) {
@ -9,5 +11,24 @@ func Init(tx *gorm.DB) (err error) {
&Connection{},
)
if opt.Debug {
err = tx.Create([]*Connection{
{
Name: "dev-minio",
Endpoint: "http://10.220.10.15:9000",
Access: "8ALV3DUZI31YG4BDRJ0Z",
Key: "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H",
},
{
Name: "test",
Endpoint: "http://10.220.10.14:19000",
Access: "5VCR05L4BSGNCTCD8DXP",
Key: "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u",
},
}).Clauses(clause.OnConflict{
DoNothing: true,
}).Error
}
return
}

View File

@ -1,24 +0,0 @@
package opt
import (
"os"
"path/filepath"
)
func Init() error {
var (
err error
)
if ConfigDir, err = os.UserConfigDir(); err != nil {
return err
}
if os.MkdirAll(filepath.Join(ConfigDir, "nf-disk"), 0755); err != nil {
return err
}
ConfigFile = filepath.Join(ConfigDir, "nf-disk", "config.db")
return nil
}

View File

@ -7,7 +7,5 @@ const (
)
var (
Debug bool = false
ConfigDir string
ConfigFile string
Debug bool = false
)

View File

@ -22,7 +22,7 @@ func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
flag.BoolVar(&opt.Debug, "debug", false, "debug mode")
flag.BoolVar(&opt.Debug, "debug", true, "debug mode")
flag.Parse()
if opt.Debug {