Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,18 @@ fn set_account_domain(state: State<'_, App>, id: String, domain: String) -> Resu
.map_err(|e| e.to_string())
}

/// 设置某账号的固定 bucket;空串清除。非空时账号根目录只显示该桶。
#[tauri::command]
fn set_account_pinned_bucket(
state: State<'_, App>,
id: String,
bucket: String,
) -> Result<(), String> {
state
.set_account_pinned_bucket(&id, &bucket)
.map_err(|e| e.to_string())
}

/// 读取账号非敏感信息(编辑回填用)。
#[tauri::command]
fn get_account(state: State<'_, App>, id: String) -> Option<AccountInfo> {
Expand Down Expand Up @@ -2158,6 +2170,7 @@ pub fn run() {
list_accounts,
list_account_infos,
set_account_domain,
set_account_pinned_bucket,
add_aliyun_account,
add_huawei_account,
add_qiniu_account,
Expand Down
64 changes: 51 additions & 13 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,18 @@ function openPdfWindow(account: string, entry: Entry) {
win.once("tauri://error", (e) => console.error("open pdf window failed", e));
}

/** 固定桶账号的有效根;普通账号的有效根仍是全部桶列表。 */
function accountRootPath(bucket?: string): string {
const name = bucket?.trim();
return name ? `${name}/` : "";
}

export default function App() {
const { t } = useI18n();
const [accounts, setAccounts] = useState<AccountInfo[]>([]);
const [current, setCurrent] = useState<string | null>(null);
const currentAccount = accounts.find((account) => account.id === current);
const accountRoot = accountRootPath(currentAccount?.pinned_bucket);
// 界面偏好(侧栏宽 / 视图 / 主题)持久化在 SQLite(ui_prefs 表)。先用默认值渲染,
// 挂载后从后端加载并回填;prefsHydrated 为真前不回写,避免用默认值覆盖已存的偏好。
const prefsHydrated = useRef(false);
Expand Down Expand Up @@ -723,12 +731,26 @@ export default function App() {
const list = await api.listAccountInfos();
setAccounts(list);
setCurrent((cur) => cur ?? list[0]?.id ?? null);
return list;
}, []);

useEffect(() => {
refreshAccounts();
}, [refreshAccounts]);

// 固定桶账号没有可发现的“全部桶”界面;首次选择账号时直接落在桶根。
useEffect(() => {
if (current || accounts.length === 0) return;
const first = accounts[0];
setCurrent(first.id);
setPath(accountRootPath(first.pinned_bucket));
}, [accounts, current]);

// 手工输入根路径或历史记录残留 "" 时,仍然立即回到固定桶根。
useEffect(() => {
if (current && accountRoot && path === "") setPath(accountRoot);
}, [accountRoot, current, path]);

useEffect(() => {
if (prefsHydrated.current)
api.setPref("sidebar_width", String(sidebarWidth)).catch(() => {});
Expand Down Expand Up @@ -992,7 +1014,11 @@ export default function App() {

const selectAccount = (id: string) => {
setCurrent(id);
setPath("");
setPath(
accountRootPath(
accounts.find((account) => account.id === id)?.pinned_bucket,
),
);
};

const addAccount = async (
Expand All @@ -1016,6 +1042,7 @@ export default function App() {
sk: string,
endpoint: string,
customDomain: string,
pinnedBucket: string,
) => {
setShowForm(false);
const oldId = editInfo?.id;
Expand Down Expand Up @@ -1046,9 +1073,15 @@ export default function App() {
await adders[vendor](id, ak, sk, endpoint);
}
await api.setAccountDomain(id, customDomain.trim());
await refreshAccounts();
await api.setAccountPinnedBucket(id, pinnedBucket.trim());
const refreshed = await refreshAccounts();
setCurrent(id);
setPath("");
setPath(
accountRootPath(
refreshed.find((account) => account.id === id)?.pinned_bucket ??
pinnedBucket,
),
);
} catch (e) {
setError(String(e));
}
Expand All @@ -1064,15 +1097,19 @@ export default function App() {
};

const removeAccount = async (id: string) => {
const wasCurrent = current === id;
await api.removeAccount(id);
if (current === id) {
setCurrent(null);
setPath("");
setEntries([]);
}
await refreshAccounts();
const nextAccounts = await refreshAccounts();
if (!wasCurrent) return;
const next = nextAccounts.find((account) => account.id !== id);
setCurrent(next?.id ?? null);
setPath(accountRootPath(next?.pinned_bucket));
setEntries([]);
};

const navigate = (nextPath: string) =>
setPath(accountRoot && nextPath === "" ? accountRoot : nextPath);

const doBatchDelete = async () => {
setPendingBatchDelete(false);
if (!current || selected.size === 0) return;
Expand Down Expand Up @@ -2003,7 +2040,7 @@ export default function App() {
} else if (matchBinding(e, keys.deleteSelected) && selected.size > 0) {
e.preventDefault();
setPendingBatchDelete(true);
} else if (matchBinding(e, keys.parent) && path !== "") {
} else if (matchBinding(e, keys.parent) && path !== accountRoot) {
e.preventDefault();
setPath(parentPath(path));
} else if (matchBinding(e, keys.refresh)) {
Expand All @@ -2025,7 +2062,7 @@ export default function App() {
const paletteCommands: PaletteCommand[] = [];
if (current)
paletteCommands.push({ id: "refresh", label: t("刷新"), run: () => void load() });
if (path !== "") {
if (path !== accountRoot) {
paletteCommands.push({
id: "up",
label: t("上一层"),
Expand Down Expand Up @@ -2136,7 +2173,7 @@ export default function App() {
{current ? (
<>
<div className="main__header">
<Breadcrumb path={path} onNavigate={setPath} />
<Breadcrumb path={path} onNavigate={navigate} />
<Bookmarks
bookmarks={bookmarks}
isBookmarked={isBookmarked}
Expand All @@ -2150,7 +2187,7 @@ export default function App() {
canForward={canForward}
onBack={() => goHistory(-1)}
onForward={() => goHistory(1)}
canGoUp={path !== ""}
canGoUp={path !== accountRoot}
canUpload={path !== ""}
busy={busy}
filter={filter}
Expand Down Expand Up @@ -2334,6 +2371,7 @@ export default function App() {
accessKeyId: editInfo.access_key_id,
endpoint: editInfo.endpoint,
customDomain: editInfo.custom_domain,
pinnedBucket: editInfo.pinned_bucket,
}
: undefined
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export const renameAccount = (id: string, newId: string) =>
export const setAccountDomain = (id: string, domain: string) =>
invoke<void>("set_account_domain", { id, domain });

/** 设置固定 bucket;空串恢复普通账号根目录。 */
export const setAccountPinnedBucket = (id: string, bucket: string) =>
invoke<void>("set_account_pinned_bucket", { id, bucket });

export const getAccount = (id: string) =>
invoke<AccountInfo | null>("get_account", { id });

Expand Down
39 changes: 27 additions & 12 deletions app/src/components/AccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Props {
accessKeyId: string;
endpoint: string;
customDomain?: string;
pinnedBucket?: string;
};
onSubmit: (
vendor: Vendor,
Expand All @@ -20,6 +21,7 @@ interface Props {
accessKeySecret: string,
endpoint: string,
customDomain: string,
pinnedBucket: string,
) => void;
onClose: () => void;
}
Expand All @@ -38,6 +40,7 @@ export function AccountForm({ initial, onSubmit, onClose }: Props) {
);
const [endpointTouched, setEndpointTouched] = useState(editing);
const [domain, setDomain] = useState(initial?.customDomain ?? "");
const [bucket, setBucket] = useState(initial?.pinnedBucket ?? "");

const meta = VENDORS[vendor];

Expand All @@ -47,17 +50,21 @@ export function AccountForm({ initial, onSubmit, onClose }: Props) {
if (!endpointTouched) setEndpoint(VENDORS[next].endpoint);
};

// 纯改别名(其它字段都没变)不强制重新输入密钥——密钥只在真的要更新凭证时才必填,
// 不然用户光是想改个名字都要翻出密钥重新输一遍。
const onlyIdChanged =
// 只有凭证或 endpoint 会参与重建 provider;改名、公共域名和固定桶可以不重输密钥。
const credentialsChanged =
editing &&
id !== initial!.id &&
ak === initial!.accessKeyId &&
endpoint === initial!.endpoint &&
domain === (initial!.customDomain ?? "");
(ak !== initial!.accessKeyId || endpoint !== initial!.endpoint);
const valid = editing
? !!id && !!ak && !!endpoint && (!!sk || onlyIdChanged)
: !!id && !!ak && !!sk && !!endpoint;
? !!id &&
!!ak &&
!!endpoint &&
(!credentialsChanged || !!sk) &&
(!bucket.trim() || !bucket.trim().includes("/"))
: !!id &&
!!ak &&
!!sk &&
!!endpoint &&
(!bucket.trim() || !bucket.trim().includes("/"));

return (
<div className="modal-backdrop" onClick={onClose}>
Expand Down Expand Up @@ -101,8 +108,8 @@ export function AccountForm({ initial, onSubmit, onClose }: Props) {
</label>
<label className="field">
<span>
{meta.skLabel}
{editing ? (onlyIdChanged ? "(仅改别名可留空)" : "(请重新输入)") : ""}
{meta.skLabel}
{editing ? (credentialsChanged ? "(请重新输入)" : "(可留空)") : ""}
</span>
<input
type="password"
Expand All @@ -129,6 +136,14 @@ export function AccountForm({ initial, onSubmit, onClose }: Props) {
placeholder="cdn.example.com — 设为公开读后用它拼永久直链"
/>
</label>
<label className="field">
<span>指定 Bucket(可选)</span>
<input
value={bucket}
onChange={(e) => setBucket(e.target.value)}
placeholder="仅有单桶权限时填写"
/>
</label>
</div>

<div className="modal__footer">
Expand All @@ -138,7 +153,7 @@ export function AccountForm({ initial, onSubmit, onClose }: Props) {
<button
className="btn btn--primary"
disabled={!valid}
onClick={() => onSubmit(vendor, id, ak, sk, endpoint, domain)}
onClick={() => onSubmit(vendor, id, ak, sk, endpoint, domain, bucket)}
>
{editing ? "保存" : "添加"}
</button>
Expand Down
2 changes: 2 additions & 0 deletions app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface AccountInfo {
endpoint: string;
/** 自定义公共域名(CDN / CNAME);空表示未配置。 */
custom_domain: string;
/** 可选固定 bucket;空表示普通多桶账号。 */
pinned_bucket: string;
}

/** 一个收藏的位置(账号 + 路径),与 Rust 端 app_core::Bookmark 对应。 */
Expand Down
2 changes: 1 addition & 1 deletion crates/app-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ provider-jdcloud = { path = "../providers/provider-jdcloud" }
provider-upyun = { path = "../providers/provider-upyun" }
bytes = { workspace = true }
futures = { workspace = true }
async-trait = "0.1"
# 断点续传上传要按偏移读本地文件分片;rt 提供 spawn_blocking(图片解码放阻塞线程池)。
tokio = { workspace = true, features = ["fs", "io-util", "rt"] }
serde = { workspace = true }
Expand All @@ -52,4 +53,3 @@ keyring = { version = "3", default-features = false, features = [
[dev-dependencies]
# test-util 提供虚拟时钟(start_paused),用于确定性地测限速。
tokio = { workspace = true, features = ["test-util"] }
async-trait = "0.1"
Loading
Loading