Resolve ROADMAP adjacency: keep both #198 and #199 entries. Authored By: TDvorak <info@tdvorak.dev> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>pull/3587/head
commit
8dcb6da51f
@ -0,0 +1,60 @@
|
|||||||
|
name: Desktop Release
|
||||||
|
|
||||||
|
# Tag `desktop-v*` builds and publishes the Tauri desktop app for
|
||||||
|
# Windows, macOS and Linux. Server releases stay on the `v*` tags
|
||||||
|
# handled by release.yml (goreleaser).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['desktop-v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundle:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: yarn
|
||||||
|
cache-dependency-path: desktop/ui/yarn.lock
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: desktop
|
||||||
|
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install UI dependencies
|
||||||
|
working-directory: desktop/ui
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
# tauri-action runs `tauri build` (which triggers beforeBuildCommand ->
|
||||||
|
# the UI build), produces the platform bundles and attaches them to the
|
||||||
|
# GitHub release for the pushed tag:
|
||||||
|
# Windows -> .msi + .exe (NSIS), macOS -> .dmg + .app.tar.gz,
|
||||||
|
# Linux -> .deb + .AppImage
|
||||||
|
# MSIX requires separate packaging + store signing; not covered here.
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
projectPath: desktop
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'Cloudreve Desktop ${{ github.ref_name }}'
|
||||||
|
releaseBody: 'Cloudreve Desktop for Linux, macOS, and Windows.'
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: false
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,153 @@
|
|||||||
|
import { Box, Container, FormControl, Grid, ListItemText, Stack } from "@mui/material";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getPublicShares } from "../../../api/api.ts";
|
||||||
|
import { Share } from "../../../api/explorer.ts";
|
||||||
|
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||||
|
import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx";
|
||||||
|
import Nothing from "../../Common/Nothing.tsx";
|
||||||
|
import { DenseSelect } from "../../Common/StyledComponents.tsx";
|
||||||
|
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||||
|
import Search from "../../Icons/Search.tsx";
|
||||||
|
import PageContainer from "../PageContainer.tsx";
|
||||||
|
import PageHeader from "../PageHeader.tsx";
|
||||||
|
import ShareCard from "../Shares/ShareCard.tsx";
|
||||||
|
|
||||||
|
const defaultPageSize = 50;
|
||||||
|
|
||||||
|
// Discover renders the public share directory: shares whose owners opted
|
||||||
|
// into public listing. Anonymous-accessible.
|
||||||
|
const Discover = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [nextPageToken, setNextPageToken] = useState<string | undefined>("");
|
||||||
|
const [shares, setShares] = useState<Share[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [orderDirection, setOrderDirection] = useState("desc");
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const loadNextPage = useCallback(
|
||||||
|
(originShares: Share[], token?: string, direction?: string, q?: string) => () => {
|
||||||
|
setLoading(true);
|
||||||
|
dispatch(
|
||||||
|
getPublicShares({
|
||||||
|
page_size: defaultPageSize,
|
||||||
|
order_direction: direction ?? orderDirection,
|
||||||
|
next_page_token: token,
|
||||||
|
query: q ?? query,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
setShares([...originShares, ...res.shares]);
|
||||||
|
setNextPageToken(res.pagination?.next_token);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setNextPageToken(undefined);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch, orderDirection, query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const refresh = useCallback(
|
||||||
|
(direction?: string, q?: string) => {
|
||||||
|
loadNextPage([], "", direction, q)();
|
||||||
|
},
|
||||||
|
[loadNextPage],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onQueryChange = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((value: string) => {
|
||||||
|
setQuery(value);
|
||||||
|
refresh(undefined, value);
|
||||||
|
}, 400),
|
||||||
|
[refresh],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => () => onQueryChange.cancel(), [onQueryChange]);
|
||||||
|
|
||||||
|
const onShareDeleted = useCallback((id: string) => {
|
||||||
|
setShares((shares) => shares.filter((share) => share.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<PageHeader
|
||||||
|
secondaryAction={
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<FormControl variant="outlined">
|
||||||
|
<DenseSelect
|
||||||
|
variant="outlined"
|
||||||
|
value={orderDirection}
|
||||||
|
onChange={(e) => {
|
||||||
|
setOrderDirection(e.target.value as string);
|
||||||
|
refresh(e.target.value as string);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SquareMenuItem value={"desc"}>
|
||||||
|
<ListItemText slotProps={{ primary: { variant: "body2" } }}>
|
||||||
|
{t("application:share.createdAtDesc")}
|
||||||
|
</ListItemText>
|
||||||
|
</SquareMenuItem>
|
||||||
|
<SquareMenuItem value={"asc"}>
|
||||||
|
<ListItemText slotProps={{ primary: { variant: "body2" } }}>
|
||||||
|
{t("application:share.createdAtAsc")}
|
||||||
|
</ListItemText>
|
||||||
|
</SquareMenuItem>
|
||||||
|
</DenseSelect>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
onRefresh={() => refresh()}
|
||||||
|
loading={loading}
|
||||||
|
title={t("application:navbar.discover")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OutlineIconTextField
|
||||||
|
variant="outlined"
|
||||||
|
icon={<Search />}
|
||||||
|
placeholder={t("application:discover.searchPlaceholder")}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
onChange={(e) => onQueryChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
{shares.map((share) => (
|
||||||
|
<ShareCard share={share} key={share.id} onShareDeleted={onShareDeleted} />
|
||||||
|
))}
|
||||||
|
{nextPageToken != undefined && (
|
||||||
|
<>
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<ShareCard
|
||||||
|
onShareDeleted={onShareDeleted}
|
||||||
|
onLoad={i == 0 ? loadNextPage(shares, nextPageToken) : undefined}
|
||||||
|
loading={true}
|
||||||
|
key={i == 0 ? nextPageToken : i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{nextPageToken == undefined && shares.length == 0 && !loading && (
|
||||||
|
<Box sx={{ p: 1, width: "100%", textAlign: "center" }}>
|
||||||
|
<Nothing size={0.8} top={63} primary={t("setting.listEmpty")} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Discover;
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImportTaskSummarizeIncludesPolicyName(t *testing.T) {
|
||||||
|
hasher, err := hashid.New("test-salt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
owner := &ent.User{ID: 1}
|
||||||
|
task, err := NewImportTask(context.Background(), owner, "/src/path", true, "cloudreve://my/dst", 7, "Local Storage")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
summary := task.Summarize(hasher)
|
||||||
|
require.NotNil(t, summary)
|
||||||
|
require.Equal(t, "Local Storage", summary.Props[SummaryKeyDstPolicyName])
|
||||||
|
require.Equal(t, hashid.EncodePolicyID(hasher, 7), summary.Props[SummaryKeySrcDstPolicyID])
|
||||||
|
require.Equal(t, "/src/path", summary.Props[SummaryKeySrcStr])
|
||||||
|
require.Equal(t, "cloudreve://my/dst", summary.Props[SummaryKeyDst])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportTaskSummarizeLegacyState(t *testing.T) {
|
||||||
|
hasher, err := hashid.New("test-salt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Tasks created before PolicyName existed have an empty name; the
|
||||||
|
// summary must still carry the encoded policy id for client-side lookup.
|
||||||
|
owner := &ent.User{ID: 1}
|
||||||
|
task, err := NewImportTask(context.Background(), owner, "/src/path", false, "cloudreve://my/dst", 7, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
summary := task.Summarize(hasher)
|
||||||
|
require.NotNil(t, summary)
|
||||||
|
require.Equal(t, "", summary.Props[SummaryKeyDstPolicyName])
|
||||||
|
require.Equal(t, hashid.EncodePolicyID(hasher, 7), summary.Props[SummaryKeySrcDstPolicyID])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportTaskFromModelSummarize(t *testing.T) {
|
||||||
|
hasher, err := hashid.New("test-salt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
owner := &ent.User{ID: 1}
|
||||||
|
created, err := NewImportTask(context.Background(), owner, "/src/path", true, "cloudreve://my/dst", 7, "S3 Backup")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Round-trip through the persisted model state.
|
||||||
|
restored := NewImportTaskFromModel(created.(*ImportTask).Task)
|
||||||
|
require.Equal(t, queue.ImportTaskType, restored.Type())
|
||||||
|
|
||||||
|
summary := restored.Summarize(hasher)
|
||||||
|
require.NotNil(t, summary)
|
||||||
|
require.Equal(t, "S3 Backup", summary.Props[SummaryKeyDstPolicyName])
|
||||||
|
}
|
||||||
Loading…
Reference in new issue