diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index b48c08e57..a6a7cc5ef 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -1,21 +1,3 @@ -# This workflow is used to create a (pre-)release build of the OpenList frontend. -# -# This will: -# -# - Update the `package.json` version to the specified version (when triggered -# by `workflow_dispatch`), commit the changes and tag it. -# - Upload the release assets to GitHub. -# - Publish the package to npm. -# -# # Usage -# -# This workflow can be triggered by: -# -# - Pushing a tag that matches the pattern `v[0-9]+.[0-9]+.[0-9]+*` (semver format). -# - Manually via the GitHub Actions UI with a version input. -# -# To create (pre-)release builds, we recommend that you use the `workflow_dispatch`. - name: Release Build on: @@ -25,9 +7,9 @@ on: workflow_dispatch: inputs: version: - description: | - Target version (e.g., 1.0.0), will create a tag named 'v' and update package.json version - required: true + description: "Target version (e.g., 1.0.0)" + required: false + default: "1.0.0" type: string jobs: @@ -37,100 +19,41 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 - with: - fetch-depth: 0 - submodules: recursive - - - name: Validate and trim semver - id: semver - uses: matt-usurp/validate-semver@v2 - with: - version: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }} - - - name: Check if version is pre-release - id: check_pre_release - run: | - if [[ -z "${{ steps.semver.outputs.prerelease }}" && -z "${{ steps.semver.outputs.build }}" ]]; then - echo "is_pre_release=false" >> $GITHUB_OUTPUT - else - echo "is_pre_release=true" >> $GITHUB_OUTPUT - fi - name: Setup Node uses: actions/setup-node@v6 with: node-version: "24.14.1" - registry-url: "https://registry.npmjs.org" - name: Install pnpm uses: pnpm/action-setup@v5 - with: - run_install: false - - name: Import GPG key - if: github.event_name == 'workflow_dispatch' - uses: crazy-max/ghaction-import-gpg@v7 - with: - gpg_private_key: ${{ secrets.BOT_GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.BOT_GPG_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - git_tag_gpgsign: true - - - name: Setup CI Bot - if: github.event_name == 'workflow_dispatch' + - name: Configure Git identity run: | - git config user.name "${{ secrets.BOT_USERNAME }}" - git config user.email "${{ secrets.BOT_USEREMAIL }}" - - - name: Update package.json and commit - if: github.event_name == 'workflow_dispatch' - run: | - jq --arg version "${{ steps.semver.outputs.version }}" '.version = $version' package.json > package.json.tmp && mv package.json.tmp package.json - git add package.json - git commit -S -m "chore: release v${{ steps.semver.outputs.version }}" --no-verify - git push - - # For local build, needn't push, the tag will be created by `gh release create` - git tag -s "v${{ steps.semver.outputs.version }}" -m "Release v${{ steps.semver.outputs.version }}" + git config user.name "GitHub Actions" + git config user.email "actions@github.com" - - name: Get current tag - id: get_current_tag + - name: Determine release tag + id: tag run: | - # Remove existing tag `rolling`, since `rolling` should always point to the latest commit - # This is necessary to avoid conflicts with the changelog generation - git tag -d rolling 2>/dev/null || true - - # Get the current tag - CURRENT_TAG=$(git describe --tags --abbrev=0) - echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT - - # Temporarily remove all pre-release tags (tags containing '-' / '+') - # This prevents them from interfering with changelog generation - PRE_RELEASE_TAGS=$(git tag -l | grep -E "(-|\+)" || true) - if [ -n "$PRE_RELEASE_TAGS" ]; then - echo "Temporarily removing pre-release tags: $PRE_RELEASE_TAGS" - echo "$PRE_RELEASE_TAGS" | xargs -r git tag -d - fi - - # Add back the current tag if is a pre-release - # Should not add `-f`, as it will overwrite the existing tag - if [[ "${{ steps.check_pre_release.outputs.is_pre_release }}" == "true" ]]; then - git tag -s "$CURRENT_TAG" -m "Release $CURRENT_TAG" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + # 使用时间戳作为 tag,避免冲突 + TIMESTAMP=$(date +%Y%m%d%H%M%S) + TAG="v${TIMESTAMP}" + echo "Creating timestamp tag: $TAG" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + echo "tag=$TAG" >> $GITHUB_OUTPUT + else + TAG="${{ github.ref_name }}" + echo "Using existing tag: $TAG" + echo "tag=$TAG" >> $GITHUB_OUTPUT fi - - name: Generate changelog - id: generate_changelog - run: | - npx changelogithub --output ${{ github.workspace }}-CHANGELOG.txt || echo "" > ${{ github.workspace }}-CHANGELOG.txt - - name: Build Release run: | chmod +x build.sh ./build.sh --release --compress - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: Move regular build run: | @@ -141,9 +64,6 @@ jobs: run: | chmod +x build.sh ./build.sh --release --compress --lite - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: Move lite build and restore regular build run: | @@ -151,46 +71,26 @@ jobs: mv dist/* lite-dist/ mv regular-dist/* dist/ - - name: Upload Release Assets + - name: Download i18n.tar.gz from latest release run: | - # Delete local tag, or gh cli will complain about it. - git tag -d ${{ steps.get_current_tag.outputs.current_tag }} - gh release create \ - --title "Release ${{ steps.get_current_tag.outputs.current_tag }}" \ - --notes-file "${{ github.workspace }}-CHANGELOG.txt" \ - --prerelease=${{ steps.check_pre_release.outputs.is_pre_release }} \ - ${{ steps.get_current_tag.outputs.current_tag }} \ - dist/openlist-frontend-dist-v*.tar.gz lite-dist/openlist-frontend-dist-lite-v*.tar.gz dist/i18n.tar.gz - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + echo "Downloading i18n.tar.gz from latest release..." + curl -L -o dist/i18n.tar.gz "https://github.com/OpenListTeam/OpenList-Frontend/releases/latest/download/i18n.tar.gz" || echo "Failed to download i18n.tar.gz" + ls -la dist/i18n.tar.gz 2>/dev/null || echo "No i18n.tar.gz downloaded" - - name: Prepare for npm + - name: Delete existing release (if any) run: | - # Delete the generated dist tarball - rm -f dist/openlist-frontend-dist-v*.tar.gz - rm -f lite-dist/openlist-frontend-dist-lite-v*.tar.gz - # Copy the lite version - mkdir dist/lite - cp -r lite-dist/. dist/lite/ - - if ! jq -e '.name and .version' package.json > /dev/null; then - echo "Error: Invalid package.json" - exit 1 + if gh release view ${{ steps.tag.outputs.tag }} --json isDraft --quiet 2>/dev/null; then + echo "Deleting existing release: ${{ steps.tag.outputs.tag }}" + gh release delete ${{ steps.tag.outputs.tag }} --cleanup-tag=false --yes 2>/dev/null || true fi - - name: Publish npm + - name: Upload Release Assets run: | - echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc - - if [ -z "${{ secrets.NPM_TOKEN }}" ]; then - echo "NPM_TOKEN not set, performing dry run" - pnpm publish --dry-run --no-git-checks --access public - else - echo "Publishing to npm..." - pnpm publish --no-git-checks --access public - fi + gh release create \ + --title "Release ${{ steps.tag.outputs.tag }}" \ + ${{ steps.tag.outputs.tag }} \ + dist/openlist-frontend-dist-v*.tar.gz \ + lite-dist/openlist-frontend-dist-lite-v*.tar.gz \ + dist/i18n.tar.gz env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - -permissions: - contents: write + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build_rolling.yml b/.github/workflows/build_rolling.yml index 3c55f866a..6ce89f345 100644 --- a/.github/workflows/build_rolling.yml +++ b/.github/workflows/build_rolling.yml @@ -47,9 +47,6 @@ jobs: run: | chmod +x build.sh ./build.sh --dev --compress - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: Read version and determine tag name id: version diff --git a/.github/workflows/i18n_sync.yml b/.github/workflows/i18n_sync.yml index 1fb126258..cf4f0f426 100644 --- a/.github/workflows/i18n_sync.yml +++ b/.github/workflows/i18n_sync.yml @@ -83,12 +83,5 @@ jobs: git push fi - - name: Sync to Crowdin - if: ${{ steps.verify-changed-files.outputs.changed == 'true' || github.event_name == 'push' }} - uses: ./.github/actions/sync_to_crowdin - with: - crowdin_project_id: ${{ secrets.CROWDIN_PROJECT_ID }} - crowdin_personal_token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - permissions: contents: write diff --git a/build.sh b/build.sh index a77ecc998..20f77327f 100755 --- a/build.sh +++ b/build.sh @@ -95,9 +95,8 @@ check_git_version_and_commit() { # Enforce git tag for release builds enforce_git_tag() { if ! git_version=$(git describe --abbrev=0 --tags 2>/dev/null); then - log_error "No git tags found. Release build requires a git tag." - log_warning "Please create a tag first, or use --dev for development builds." - exit 1 + log_warning "No git tags found. Using version from package.json." + git_version="v$(grep '"version":' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')" fi validate_git_tag } @@ -106,6 +105,13 @@ enforce_git_tag() { validate_git_tag() { package_version=$(grep '"version":' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/') git_version_clean=${git_version#v} + + # 检查是否为时间戳格式(纯数字),如果是则跳过版本验证 + if [[ "$git_version_clean" =~ ^[0-9]+$ ]]; then + log_info "Timestamp tag detected (${git_version_clean}), skipping version validation" + return 0 + fi + if [[ "$git_version_clean" != "$package_version" ]]; then log_error "Package.json version (${package_version}) does not match git tag (${git_version_clean})." exit 1 @@ -146,14 +152,7 @@ build_project() { log_step "==== Installing dependencies ====" pnpm install - log_step "==== Building i18n ====" - if [[ "$SKIP_I18N" == "false" ]]; then - pnpm i18n:release - else - fetch_i18n_from_release - fi - - log_step "==== Building project ====" + log_step "==== Building project (English only, no crowdin) ====" if [[ "$LITE_FLAG" == "true" ]]; then pnpm build:lite else diff --git a/package.json b/package.json index 0c9e0b89c..4688a95ff 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,6 @@ ], "homepage": "https://openlist.team/", "scripts": { - "crowdin:upload": "crowdin upload sources --auto-update", - "crowdin:download": "crowdin download --verbose", - "crowdin": "pnpm crowdin:upload && pnpm crowdin:download", - "i18n:build": "pnpm crowdin && node ./scripts/i18n.mjs", - "i18n:release": "pnpm run crowdin:download && node ./scripts/i18n.mjs", "start": "vite", "dev": "vite --force", "build": "vite build", diff --git a/src/app/i18n.ts b/src/app/i18n.ts index 21fb518e4..d8af7912d 100644 --- a/src/app/i18n.ts +++ b/src/app/i18n.ts @@ -2,51 +2,36 @@ import * as i18n from "@solid-primitives/i18n" import { createResource, createSignal } from "solid-js" export { i18n } -// glob search by Vite -const langs = import.meta.glob("~/lang/*/index.json", { - eager: true, - import: "lang", -}) - -// all available languages -export const languages = Object.keys(langs).map((langPath) => { - const langCode = langPath.split("/")[3] - const langName = langs[langPath] as string - return { code: langCode, lang: langName } -}) - -// determine browser's default language -const userLang = navigator.language.toLowerCase() -const defaultLang = - languages.find((lang) => lang.code.toLowerCase() === userLang)?.code || - languages.find( - (lang) => lang.code.toLowerCase().split("-")[0] === userLang.split("-")[0], - )?.code || - "en" - -// Get initial language from localStorage or fallback to defaultLang -export let initialLang = localStorage.getItem("lang") ?? defaultLang - -if (!languages.some((lang) => lang.code === initialLang)) { - initialLang = defaultLang +// Only use English as the default language (no crowdin, single language mode) +const langs = { + "~/lang/en/index.json": "English", } +// all available languages (only English) +export const languages = [{ code: "en", lang: "English" }] + +// Always use English as the default language +const defaultLang = "en" + +// Get initial language - always English +export let initialLang = "en" + // Type imports // use `type` to not include the actual dictionary in the bundle import type * as en from "~/lang/en/entry" -export type Lang = keyof typeof langs +export type Lang = "en" export type RawDictionary = typeof en.dict export type Dictionary = i18n.Flatten -// Fetch and flatten the dictionary -const fetchDictionary = async (locale: Lang): Promise => { +// Fetch and flatten the dictionary (only English) +const fetchDictionary = async (_locale: Lang): Promise => { try { - const dict: RawDictionary = (await import(`~/lang/${locale}/entry.ts`)).dict + const dict: RawDictionary = (await import(`~/lang/en/entry.ts`)).dict return i18n.flatten(dict) // Flatten dictionary for easier access to keys } catch (err) { - console.error(`Error loading dictionary for locale: ${locale}`, err) - throw new Error(`Failed to load dictionary for ${locale}`) + console.error(`Error loading dictionary for locale: English`, err) + throw new Error(`Failed to load dictionary for English`) } } diff --git a/src/components/ImageWithError.tsx b/src/components/ImageWithError.tsx index a5b91675e..3619b3786 100644 --- a/src/components/ImageWithError.tsx +++ b/src/components/ImageWithError.tsx @@ -4,6 +4,7 @@ import { createSignal, JSXElement, Show } from "solid-js" export const ImageWithError = ( props: ImageProps & { fallbackErr?: JSXElement + onLoad?: () => void }, ) => { const [err, setErr] = createSignal(false) @@ -11,6 +12,7 @@ export const ImageWithError = ( { setErr(true) }} diff --git a/src/pages/home/folder/GridItem.tsx b/src/pages/home/folder/GridItem.tsx index abaa550e8..aa0eb61b7 100644 --- a/src/pages/home/folder/GridItem.tsx +++ b/src/pages/home/folder/GridItem.tsx @@ -1,7 +1,7 @@ import { Center, VStack, Icon, Text } from "@hope-ui/solid" import { Motion } from "solid-motionone" import { useContextMenu } from "solid-contextmenu" -import { batch, Show } from "solid-js" +import { batch, Show, createSignal, onMount, onCleanup } from "solid-js" import { CenterLoading, LinkWithPush, ImageWithError } from "~/components" import { usePath, useRouter, useUtil } from "~/hooks" import { checkboxOpen, getMainColor, local, selectIndex } from "~/store" @@ -27,8 +27,37 @@ export const GridItem = (props: { obj: StoreObj; index: number }) => { const { pushHref, to } = useRouter() const { openWithDoubleClick, toggleWithClick, restoreSelectionCache } = useSelectWithMouse() + + const [isVisible, setIsVisible] = createSignal(false) + const [loaded, setLoaded] = createSignal(false) + const [canLoad, setCanLoad] = createSignal(false) + let ref: HTMLDivElement | undefined + let loadTimeout: number | undefined + + onMount(() => { + if (ref) { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + loadTimeout = setTimeout(() => setCanLoad(true), 500) + } else { + if (loadTimeout) clearTimeout(loadTimeout) + setCanLoad(false) + } + setIsVisible(entry.isIntersecting) + }, + { rootMargin: "50px" }, + ) + observer.observe(ref) + onCleanup(() => { + observer.disconnect() + if (loadTimeout) clearTimeout(loadTimeout) + }) + } + }) return ( { }} /> - + { fallbackErr={objIcon} src={props.obj.thumb} loading="lazy" + onLoad={() => setLoaded(true)} />