Compare commits
No commits in common. "master" and "dev" have entirely different histories.
9
.eslintrc.js
Normal file
9
.eslintrc.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@nextcloud',
|
||||
],
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'vue/first-attribute-linebreak': 'off',
|
||||
},
|
||||
}
|
50
.github/dependabot.yml
vendored
Normal file
50
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: composer
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: composer
|
||||
directory: "/vendor-bin/cs-fixer"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: composer
|
||||
directory: "/vendor-bin/openapi-extractor"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: composer
|
||||
directory: "/vendor-bin/phpunit"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: composer
|
||||
directory: "/vendor-bin/psalm"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
31
.github/workflows/block-unconventional-commits.yml
vendored
Normal file
31
.github/workflows/block-unconventional-commits.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Block unconventional commits
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: block-unconventional-commits-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
block-unconventional-commits:
|
||||
name: Block unconventional commits
|
||||
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: webiny/action-conventional-commits@8bc41ff4e7d423d56fa4905f6ff79209a78776c7 # v1.3.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
33
.github/workflows/fixup.yml
vendored
Normal file
33
.github/workflows/fixup.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Block fixup and squash commits
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: fixup-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
commit-message-check:
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
name: Block fixup and squash commits
|
||||
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
steps:
|
||||
- name: Run check
|
||||
uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
95
.github/workflows/lint-eslint.yml
vendored
Normal file
95
.github/workflows/lint-eslint.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
#
|
||||
# Use lint-eslint together with lint-eslint-when-unrelated to make eslint a required check for GitHub actions
|
||||
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
|
||||
|
||||
name: Lint eslint
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: lint-eslint-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
outputs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- '.github/workflows/**'
|
||||
- 'src/**'
|
||||
- 'appinfo/info.xml'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- '.eslintrc.*'
|
||||
- '.eslintignore'
|
||||
- '**.js'
|
||||
- '**.ts'
|
||||
- '**.vue'
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src != 'false'
|
||||
|
||||
name: NPM lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
|
||||
id: versions
|
||||
with:
|
||||
fallbackNode: '^20'
|
||||
fallbackNpm: '^10'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, lint]
|
||||
|
||||
if: always()
|
||||
|
||||
# This is the summary, we just avoid to rename it so that branch protection rules still match
|
||||
name: eslint
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi
|
33
.github/workflows/lint-info-xml.yml
vendored
Normal file
33
.github/workflows/lint-info-xml.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Lint info.xml
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: lint-info-xml-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
xml-linters:
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
name: info.xml lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Download schema
|
||||
run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd
|
||||
|
||||
- name: Lint info.xml
|
||||
uses: ChristophWurst/xmllint-action@36f2a302f84f8c83fceea0b9c59e1eb4a616d3c1 # v1.2
|
||||
with:
|
||||
xml-file: ./appinfo/info.xml
|
||||
xml-schema-file: ./info.xsd
|
45
.github/workflows/lint-php-cs.yml
vendored
Normal file
45
.github/workflows/lint-php-cs.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Lint php-cs
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: lint-php-cs-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: php-cs
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Get php version
|
||||
id: versions
|
||||
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
|
||||
|
||||
- name: Set up php${{ steps.versions.outputs.php-available }}
|
||||
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
|
||||
with:
|
||||
php-version: ${{ steps.versions.outputs.php-available }}
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: none
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer i
|
||||
|
||||
- name: Lint
|
||||
run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )
|
67
.github/workflows/lint-php.yml
vendored
Normal file
67
.github/workflows/lint-php.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Lint php
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: lint-php-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
runs-on: ubuntu-latest-low
|
||||
outputs:
|
||||
php-versions: ${{ steps.versions.outputs.php-versions }}
|
||||
steps:
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Get version matrix
|
||||
id: versions
|
||||
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.0.0
|
||||
|
||||
php-lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: matrix
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}}
|
||||
|
||||
name: php-lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: none
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Lint
|
||||
run: composer run lint
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: php-lint
|
||||
|
||||
if: always()
|
||||
|
||||
name: php-lint-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi
|
48
.github/workflows/lint-stylelint.yml
vendored
Normal file
48
.github/workflows/lint-stylelint.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Lint stylelint
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: lint-stylelint-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: stylelint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
|
||||
id: versions
|
||||
with:
|
||||
fallbackNode: '^20'
|
||||
fallbackNpm: '^10'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run stylelint
|
73
.github/workflows/npm-audit-fix.yml
vendored
Normal file
73
.github/workflows/npm-audit-fix.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Npm audit fix and compile
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 2:30 on Sundays
|
||||
- cron: '30 2 * * 0'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branches: ['main', 'master', 'stable29', 'stable28', 'stable27']
|
||||
|
||||
name: npm-audit-fix-${{ matrix.branches }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ matrix.branches }}
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
|
||||
id: versions
|
||||
with:
|
||||
fallbackNode: '^20'
|
||||
fallbackNpm: '^10'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Fix npm audit
|
||||
run: |
|
||||
npm audit fix
|
||||
|
||||
- name: Run npm ci and npm run build
|
||||
if: always()
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
commit-message: "fix(deps): fix npm audit"
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
|
||||
signoff: true
|
||||
branch: automated/noid/${{ matrix.branches }}-fix-npm-audit
|
||||
title: "[${{ matrix.branches }}] Fix npm audit"
|
||||
body: |
|
||||
Auto-generated fix of npm audit
|
||||
labels: |
|
||||
dependencies
|
||||
3. to review
|
85
.github/workflows/openapi.yml
vendored
Normal file
85
.github/workflows/openapi.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
name: OpenAPI
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: openapi-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
openapi:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Get php version
|
||||
id: php_versions
|
||||
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
|
||||
with:
|
||||
php-version: ${{ steps.php_versions.outputs.php-available }}
|
||||
extensions: xml
|
||||
coverage: none
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check Typescript OpenApi types
|
||||
id: check_typescript_openapi
|
||||
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
|
||||
with:
|
||||
files: "src/types/openapi/openapi*.ts"
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
if: steps.check_typescript_openapi.outputs.files_exists == 'true'
|
||||
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
|
||||
id: node_versions
|
||||
# Continue if no package.json
|
||||
continue-on-error: true
|
||||
with:
|
||||
fallbackNode: '^20'
|
||||
fallbackNpm: '^10'
|
||||
|
||||
- name: Set up node ${{ steps.node_versions.outputs.nodeVersion }}
|
||||
if: ${{ steps.node_versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: ${{ steps.node_versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.node_versions.outputs.npmVersion }}
|
||||
if: ${{ steps.node_versions.outputs.nodeVersion }}
|
||||
run: npm i -g npm@"${{ steps.node_versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: ${{ steps.node_versions.outputs.nodeVersion }}
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Set up dependencies
|
||||
run: composer i
|
||||
|
||||
- name: Regenerate OpenAPI
|
||||
run: composer run openapi
|
||||
|
||||
- name: Check openapi*.json and typescript changes
|
||||
run: |
|
||||
bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please run \"composer run openapi\" and commit the openapi*.json files and (if applicable) src/types/openapi/openapi*.ts, see the section \"Show changes on failure\" for details' && exit 1)"
|
||||
|
||||
- name: Show changes on failure
|
||||
if: failure()
|
||||
run: |
|
||||
git status
|
||||
git --no-pager diff
|
||||
exit 1 # make it red to grab attention
|
68
.github/workflows/psalm-matrix.yml
vendored
Normal file
68
.github/workflows/psalm-matrix.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Static analysis
|
||||
|
||||
on: pull_request
|
||||
|
||||
concurrency:
|
||||
group: psalm-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
runs-on: ubuntu-latest-low
|
||||
outputs:
|
||||
ocp-matrix: ${{ steps.versions.outputs.ocp-matrix }}
|
||||
steps:
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Get version matrix
|
||||
id: versions
|
||||
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
|
||||
|
||||
static-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
needs: matrix
|
||||
strategy:
|
||||
# do not stop on another job's failure
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.matrix.outputs.ocp-matrix) }}
|
||||
|
||||
name: static-psalm-analysis ${{ matrix.ocp-version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up php${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: none
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer i
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer require --dev nextcloud/ocp:${{ matrix.ocp-version }} --ignore-platform-reqs --with-dependencies
|
||||
|
||||
- name: Run coding standards check
|
||||
run: composer run psalm
|
||||
|
||||
summary:
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: static-analysis
|
||||
|
||||
if: always()
|
||||
|
||||
name: static-psalm-analysis-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.static-analysis.result != 'success' }}; then exit 1; fi
|
49
.github/workflows/update-nextcloud-ocp-approve-merge.yml
vendored
Normal file
49
.github/workflows/update-nextcloud-ocp-approve-merge.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Auto approve nextcloud/ocp
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- stable*
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: update-nextcloud-ocp-approve-merge-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-approve-merge:
|
||||
if: github.actor == 'nextcloud-command'
|
||||
runs-on: ubuntu-latest-low
|
||||
permissions:
|
||||
# for hmarr/auto-approve-action to approve PRs
|
||||
pull-requests: write
|
||||
# for alexwilson/enable-github-automerge-action to approve PRs
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: mdecoleman/pr-branch-name@bab4c71506bcd299fb350af63bb8e53f2940a599 # v2.0.0
|
||||
id: branchname
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# GitHub actions bot approve
|
||||
- uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2
|
||||
if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Enable GitHub auto merge
|
||||
- name: Auto merge
|
||||
uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # main
|
||||
if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
94
.github/workflows/update-nextcloud-ocp-matrix.yml
vendored
Normal file
94
.github/workflows/update-nextcloud-ocp-matrix.yml
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Update nextcloud/ocp
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '5 2 * * 0'
|
||||
|
||||
jobs:
|
||||
update-nextcloud-ocp:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branches: ['main']
|
||||
target: ['stable29']
|
||||
|
||||
name: update-nextcloud-ocp-${{ matrix.branches }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ matrix.branches }}
|
||||
submodules: true
|
||||
|
||||
- name: Set up php8.2
|
||||
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
# https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: none
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Read codeowners
|
||||
id: codeowners
|
||||
run: |
|
||||
grep '/appinfo/info.xml' .github/CODEOWNERS | cut -f 2- -d ' ' | xargs | awk '{ print "codeowners="$0 }' >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
- name: Composer install
|
||||
run: composer install
|
||||
|
||||
- name: Composer update nextcloud/ocp
|
||||
id: update_branch
|
||||
run: composer require --dev nextcloud/ocp:dev-${{ matrix.target }}
|
||||
|
||||
- name: Raise on issue on failure
|
||||
uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0
|
||||
if: ${{ failure() && steps.update_branch.conclusion == 'failure' }}
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
title: Failed to update nextcloud/ocp package}
|
||||
body: Please check the output of the GitHub action and manually resolve the issues<br>${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}<br>${{ steps.codeowners.outputs.codeowners }}
|
||||
|
||||
- name: Reset checkout 3rdparty
|
||||
run: |
|
||||
git clean -f 3rdparty
|
||||
git checkout 3rdparty
|
||||
continue-on-error: true
|
||||
|
||||
- name: Reset checkout vendor
|
||||
run: |
|
||||
git clean -f vendor
|
||||
git checkout vendor
|
||||
continue-on-error: true
|
||||
|
||||
- name: Reset checkout vendor-bin
|
||||
run: |
|
||||
git clean -f vendor-bin
|
||||
git checkout vendor-bin
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
commit-message: "chore(dev-deps): Bump nextcloud/ocp package"
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
|
||||
signoff: true
|
||||
branch: automated/noid/${{ matrix.branches }}-update-nextcloud-ocp
|
||||
title: "[${{ matrix.branches }}] Update nextcloud/ocp dependency"
|
||||
body: |
|
||||
Auto-generated update of [nextcloud/ocp](https://github.com/nextcloud-deps/ocp/) dependency
|
||||
labels: |
|
||||
dependencies
|
||||
3. to review
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
/.idea/
|
||||
/*.iml
|
||||
|
||||
/vendor/
|
||||
/vendor-bin/*/vendor/
|
||||
|
||||
/.php-cs-fixer.cache
|
||||
/tests/.phpunit.cache
|
||||
|
||||
/node_modules/
|
||||
|
||||
/**/*Zone.Identifier
|
||||
|
||||
/js/
|
||||
**/output.css
|
19
.php-cs-fixer.dist.php
Normal file
19
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once './vendor-bin/cs-fixer/vendor/autoload.php';
|
||||
|
||||
use Nextcloud\CodingStandard\Config;
|
||||
|
||||
$config = new Config();
|
||||
$config
|
||||
->getFinder()
|
||||
->notPath('build')
|
||||
->notPath('l10n')
|
||||
->notPath('node_modules')
|
||||
->notPath('src')
|
||||
->notPath('vendor')
|
||||
->in(__DIR__);
|
||||
|
||||
return $config;
|
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- First release
|
9
CODE_OF_CONDUCT.md
Normal file
9
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,9 @@
|
||||
In the Nextcloud community, participants from all over the world come together to create Free Software for a free internet. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use Nextcloud software.
|
||||
|
||||
Our code of conduct offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other.
|
||||
|
||||
The Code of Conduct is shared by all contributors and users who engage with the Nextcloud team and its community services. It presents a summary of the shared values and “common sense” thinking in our community.
|
||||
|
||||
You can find our full code of conduct on our website: https://nextcloud.com/code-of-conduct/
|
||||
|
||||
Please, keep our CoC in mind when you contribute! That way, everyone can be a part of our community in a productive, positive, creative and fun way.
|
68
README.md
68
README.md
@ -1,3 +1,67 @@
|
||||
# ArchiveLoaderNC
|
||||
# Application Webtransfer
|
||||
|
||||
Classement d'archives sur NextCloud
|
||||
## Compilation du JavaScript
|
||||
|
||||
Une fois l'application clonée depuis le dépôt Git, suivez les étapes ci-dessous pour construire les fichiers nécessaires :
|
||||
|
||||
1. Assurez-vous que toutes les dépendances sont installées en exécutant la commande suivante à la racine du projet :
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Exécutez le script `buildAll` défini dans le fichier `package.json` avec la commande suivante :
|
||||
```bash
|
||||
npm run buildAll
|
||||
```
|
||||
|
||||
### Remarque importante
|
||||
Lorsque l'application sera publiée sur l'App Store de Nextcloud, il faudra vérifier le processus de build automatique des applications. La méthode actuelle (`npm run buildAll`) pourrait ne pas être compatible avec les pipelines de build utilisés pour l'App Store.
|
||||
|
||||
Nous recommandons de documenter ce processus ou d'adapter le workflow en conséquence une fois les exigences de l'App Store clarifiées.
|
||||
|
||||
---
|
||||
|
||||
## Dépendances et outils nécessaires
|
||||
|
||||
Pour pouvoir construire et exécuter l'application, vous devez disposer des outils suivants :
|
||||
|
||||
- **Node.js**
|
||||
- **npm** : Livré avec Node.js
|
||||
|
||||
Assurez-vous que ces outils sont correctement installés avant de procéder à la compilation.
|
||||
|
||||
---
|
||||
|
||||
## Scripts disponibles
|
||||
|
||||
Le fichier `package.json` contient plusieurs scripts utiles pour le développement et le déploiement de l'application. Voici une liste des principaux scripts et leur rôle :
|
||||
|
||||
- **`build`** : Compile l'application en mode production.
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
- **`dev`** : Compile l'application en mode développement.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- **`watch`** : Surveille les fichiers pour des modifications et reconstruit automatiquement.
|
||||
```bash
|
||||
npm run watch
|
||||
```
|
||||
|
||||
- **`lint`** : Analyse le code source pour détecter les erreurs avec ESLint.
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
- **`stylelint`** : Analyse les fichiers CSS/SCSS/Vue pour détecter des erreurs de style.
|
||||
```bash
|
||||
npm run stylelint
|
||||
```
|
||||
|
||||
- **`tailwind`** : Génère le fichier CSS principal (`output.css`) à partir des classes Tailwind.
|
||||
```bash
|
||||
npm run tailwind
|
||||
|
17
appinfo/info.xml
Normal file
17
appinfo/info.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>webtransfer</id>
|
||||
<name>Web Transfer</name>
|
||||
<summary>Allow users to transfer files from another repository</summary>
|
||||
<description>Allow users to transfer files from another repository</description>
|
||||
<version>1.0.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author mail="guillaume.marrec.frey@proton.me" homepage="">Guillaume Marrec</author>
|
||||
<namespace>WebTransfer</namespace>
|
||||
<category>files</category>
|
||||
<bugs>https://gmrrc.fr</bugs>
|
||||
<dependencies>
|
||||
<nextcloud min-version="29" max-version="29"/>
|
||||
</dependencies>
|
||||
</info>
|
8
appinfo/routes.php
Normal file
8
appinfo/routes.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'page#main', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#zipDrop', 'url' => '/zipDrop', 'verb' => 'GET'],
|
||||
['name' => 'page#getZipFile', 'url' => '/getZipFile', 'verb' => 'GET']
|
||||
]
|
||||
];
|
49
composer.json
Normal file
49
composer.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "nextcloud/webserver",
|
||||
"description": "Allow users to transfer files from another repository",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Guillaume Marrec",
|
||||
"email": "guillaume.marrec.frey@proton.me",
|
||||
"homepage": ""
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OCA\\WebServer\\": "lib/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"@composer bin all install --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@composer bin all update --ansi"
|
||||
],
|
||||
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
|
||||
"cs:check": "php-cs-fixer fix --dry-run --diff",
|
||||
"cs:fix": "php-cs-fixer fix",
|
||||
"psalm": "psalm --threads=1 --no-cache",
|
||||
"test:unit": "phpunit tests -c tests/phpunit.xml --colors=always --fail-on-warning --fail-on-risky",
|
||||
"openapi": "generate-spec"
|
||||
},
|
||||
"require": {
|
||||
"bamarni/composer-bin-plugin": "^1.8",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nextcloud/ocp": "dev-stable29",
|
||||
"roave/security-advisories": "dev-latest"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"bamarni/composer-bin-plugin": true
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.1"
|
||||
}
|
||||
}
|
||||
}
|
1
img/app-dark.svg
Normal file
1
img/app-dark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20Z" /></svg>
|
After Width: | Height: | Size: 302 B |
1
img/app.svg
Normal file
1
img/app.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20Z" /></svg>
|
After Width: | Height: | Size: 315 B |
25
lib/AppInfo/Application.php
Normal file
25
lib/AppInfo/Application.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\WebTransfer\AppInfo;
|
||||
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'webtransfer';
|
||||
|
||||
/** @psalm-suppress PossiblyUnusedMethod */
|
||||
public function __construct() {
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
31
lib/Controller/ApiController.php
Normal file
31
lib/Controller/ApiController.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\WebTransfer\Controller;
|
||||
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\ApiRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ApiController extends OCSController {
|
||||
/**
|
||||
* An example API endpoint
|
||||
*
|
||||
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>
|
||||
*
|
||||
* 200: Data returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
#[ApiRoute(verb: 'GET', url: '/api')]
|
||||
public function index(): DataResponse {
|
||||
return new DataResponse(
|
||||
['message' => 'Hello world!']
|
||||
);
|
||||
}
|
||||
}
|
129
lib/Controller/PageController.php
Normal file
129
lib/Controller/PageController.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\WebTransfer\Controller;
|
||||
|
||||
use OCA\WebTransfer\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IRequest;
|
||||
use OCP\IResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class PageController extends Controller {
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
|
||||
#[FrontpageRoute(verb: 'GET', url: '/')]
|
||||
public function index(): TemplateResponse {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'index',
|
||||
);
|
||||
}
|
||||
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
|
||||
#[FrontpageRoute(verb: 'GET', url: '/zipDrop')]
|
||||
public function zipDrop() {
|
||||
// Récupérer le paramètre subUrl (compatible GET et POST)
|
||||
$subUrl = $this->request->getParam('subUrl');
|
||||
|
||||
if (!$subUrl) {
|
||||
return new \OCP\AppFramework\Http\DataResponse([
|
||||
'error' => 'Le paramètre subUrl est manquant'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Optionnel : Validation de l'URL
|
||||
if (filter_var($subUrl, FILTER_VALIDATE_URL) === false) {
|
||||
return new \OCP\AppFramework\Http\DataResponse([
|
||||
'error' => 'subUrl n\'est pas une URL valide'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$parameters = array('archiveUrl' => $subUrl);
|
||||
|
||||
// Réponse de succès
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'index',
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
|
||||
#[FrontpageRoute(verb: 'GET', url: '/getZipFile')]
|
||||
public function getZipFile() {
|
||||
// Récupérer les données envoyées dans la requête
|
||||
$zipUrl = $this->request->getParam('subUrl');
|
||||
|
||||
// Initialiser les paramètres de réponse
|
||||
$parameters = [
|
||||
'status' => 'error', // Par défaut, la réponse indique une erreur
|
||||
'message' => '',
|
||||
'data' => null
|
||||
];
|
||||
|
||||
// Valider l'URL
|
||||
if (!$zipUrl || filter_var($zipUrl, FILTER_VALIDATE_URL) === false) {
|
||||
$parameters['message'] = 'Invalid URL';
|
||||
return new JsonResponse($parameters, 400); // 400 Bad Request
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialiser cURL
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $zipUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // Timeout de 10 secondes
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Suivre les redirections
|
||||
|
||||
// Récupérer le contenu
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Gérer les erreurs cURL
|
||||
if (curl_errno($ch)) {
|
||||
$parameters['message'] = 'cURL error: ' . curl_error($ch);
|
||||
curl_close($ch);
|
||||
return new JsonResponse(['parameters' => $parameters, 'status' => 500]); // 500 Internal Server Error
|
||||
}
|
||||
|
||||
// Vérifier le code HTTP
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$parameters['message'] = "HTTP error: $httpCode";
|
||||
return new JsonResponse(['parameters' => $parameters, 'status' => $httpCode]);
|
||||
}
|
||||
|
||||
// Encodage explicite en UTF-8 si nécessaire
|
||||
if (!mb_detect_encoding($response, 'UTF-8', true)) {
|
||||
$response = utf8_encode($response);
|
||||
}
|
||||
|
||||
// Si tout est OK, construire la réponse
|
||||
$parameters['status'] = 'success';
|
||||
$parameters['message'] = 'File retrieved successfully';
|
||||
$parameters['data'] = $response; // Encodage Base64 pour éviter les problèmes d'encodage JSON
|
||||
|
||||
return new JsonResponse(['parameters' => $parameters, 'status' => 200]); // 200 OK
|
||||
} catch (\Exception $e) {
|
||||
$parameters['message'] = 'Exception: ' . $e->getMessage();
|
||||
return new JsonResponse(['parameters' => $parameters, 'status' => 500]); // 500 Internal Server Error
|
||||
}
|
||||
}
|
||||
}
|
121
openapi.json
Normal file
121
openapi.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "webserver",
|
||||
"version": "0.0.1",
|
||||
"description": "Allow users to transfer files from another repository",
|
||||
"license": {
|
||||
"name": "agpl"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"basic_auth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
},
|
||||
"bearer_auth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"OCSMeta": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"statuscode"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"statuscode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"totalitems": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemsperpage": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/ocs/v2.php/apps/webserver/api": {
|
||||
"get": {
|
||||
"operationId": "api-index",
|
||||
"summary": "An example API endpoint",
|
||||
"tags": [
|
||||
"api"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Data returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
15983
package-lock.json
generated
Normal file
15983
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "webserver",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": "^20.0.0",
|
||||
"npm": "^10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --config webpack.js --progress",
|
||||
"dev": "NODE_ENV=development webpack --config webpack.js --progress",
|
||||
"watch": "NODE_ENV=development webpack --config webpack.js --progress --watch",
|
||||
"lint": "eslint src",
|
||||
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css",
|
||||
"tailwind": "npx tailwindcss -i ./src/input.css -o ./src/output.css",
|
||||
"buildAll": "npm i && npm run tailwind && npm run build"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@nextcloud/dialogs": "^3.1.2",
|
||||
"@nextcloud/files": "^3.10.0",
|
||||
"@nextcloud/initial-state": "^2.2.0",
|
||||
"@nextcloud/vue": "^8.20.0",
|
||||
"file-type": "^19.6.0",
|
||||
"i18next": "^24.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"vue": "^2.7.16",
|
||||
"vue-material-design-icons": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/browserslist-config": "^3.0.1",
|
||||
"@nextcloud/eslint-config": "^8.3.0",
|
||||
"@nextcloud/stylelint-config": "^2.4.0",
|
||||
"@nextcloud/webpack-vue-config": "^6.0.1",
|
||||
"eslint-webpack-plugin": "^4.1.0",
|
||||
"stylelint-webpack-plugin": "^5.0.0",
|
||||
"tailwindcss": "^3.4.15"
|
||||
}
|
||||
}
|
20
psalm.xml
Normal file
20
psalm.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
findUnusedBaselineEntry="true"
|
||||
findUnusedCode="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="lib" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
<extraFiles>
|
||||
<directory name="vendor"/>
|
||||
</extraFiles>
|
||||
</psalm>
|
1
src/.gitignore
vendored
Normal file
1
src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/output.css
|
66
src/App.vue
Normal file
66
src/App.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div id="app" class="h-full w-full dark:bg-black/80 bg-white/80">
|
||||
<!-- Conteneur principal, ajustement en flex-row à partir de sm -->
|
||||
<div class="h-full w-full flex flex-col sm:flex-row">
|
||||
<!-- Première section -->
|
||||
<div
|
||||
class="w-full sm:w-1/3 max-sm:h-2/5 p-4 sm:m-6 sm:mr-0 rounded-xl dark:bg-NcBlack/40 bg-white/80">
|
||||
<WebContentViewer :translate="translate" @file-upload="handleFileUpload" @dragEnded="toggleDragEnded" :zipUrl="zipUrl"/>
|
||||
</div>
|
||||
<!-- Deuxième section -->
|
||||
<div
|
||||
class="w-full sm:w-2/3 max-sm:h-3/5 p-4 sm:m-6 sm:ml-4 dark:bg-NcBlack bg-white rounded-xl">
|
||||
<FileTable :file="sharedFile" :dragEnded="dragEnded" :translate="translate" @dragEnded="toggleDragEnded"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileTable from './components/FileTable.vue';
|
||||
import WebContentViewer from './components/WebContentViewer.vue';
|
||||
import './output.css';
|
||||
|
||||
// Traduction
|
||||
import i18next from "i18next";
|
||||
import file from "./assets/traduction.json";
|
||||
|
||||
await i18next.init({
|
||||
lng: navigator.language.split('-')[0],
|
||||
fallbackLng: "en",
|
||||
resources: file,
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
FileTable,
|
||||
WebContentViewer
|
||||
},
|
||||
data() {
|
||||
let zipUrl = document.getElementById('archiveInfos').getAttribute('dataarchiveurl');
|
||||
//console.log(zipUrl)
|
||||
return {
|
||||
zipUrl,
|
||||
sharedFile: null,
|
||||
dragEnded: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleFileUpload(file) {
|
||||
this.sharedFile = file;
|
||||
},
|
||||
toggleDragEnded(){
|
||||
this.dragEnded = !this.dragEnded;
|
||||
this.sharedFile = null;
|
||||
},
|
||||
translate(id) {
|
||||
return i18next.t(id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
1
src/assets/file.svg
Normal file
1
src/assets/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 16 16" height="16" width="16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M6 22c-.55 0-1.021-.196-1.412-.587A1.927 1.927 0 0 1 4 20V4c0-.55.196-1.021.588-1.413A1.926 1.926 0 0 1 6 2h8l6 6v12a1.93 1.93 0 0 1-.587 1.413A1.93 1.93 0 0 1 18 22H6Z" style="fill:#969696;fill-rule:nonzero" transform="matrix(.7 0 0 .7 -.43 -.388)"/></svg>
|
After Width: | Height: | Size: 456 B |
1
src/assets/package-x-generic.svg
Normal file
1
src/assets/package-x-generic.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#969696" d="M5.12,5H18.87L17.93,4H5.93L5.12,5M20.54,5.23C20.83,5.57 21,6 21,6.5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6.5C3,6 3.17,5.57 3.46,5.23L4.84,3.55C5.12,3.21 5.53,3 6,3H18C18.47,3 18.88,3.21 19.15,3.55L20.54,5.23M6,18H12V15H6V18Z"/></svg>
|
After Width: | Height: | Size: 339 B |
200
src/assets/traduction.json
Normal file
200
src/assets/traduction.json
Normal file
@ -0,0 +1,200 @@
|
||||
{
|
||||
"en": {
|
||||
"translation": {
|
||||
"all.files" : "All files",
|
||||
"favorites": "Favorites",
|
||||
"no.content": "No content to display",
|
||||
"enter.file.name": "Enter the name of the file",
|
||||
"modify.file.name": "Modify the name of the file",
|
||||
"you.are.going.to.erase.file.folder" : "You are going to erase the file/folder",
|
||||
"apply.to.all.*" : "Apply to all*",
|
||||
"*.text" : "* You can't undo this action",
|
||||
"create.new.file": "Create a new file",
|
||||
"name.of.file": "Name of the file",
|
||||
"change.file.name": "Change the name of the file",
|
||||
"file.already.exist" : "The file already exists",
|
||||
"file.pt.1" : "The file \"",
|
||||
"file.pt.2" : "\" already exists, what do you want to do ?",
|
||||
"cant.rename": "You can't rename the file/folder : ",
|
||||
"cant.create.folder": "You can't create the folder : ",
|
||||
"already.exists": ", because it already exists.",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"type": "Type",
|
||||
"options": "Options",
|
||||
"new": "New",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"create": "Create",
|
||||
"overwrite": "Overwrite",
|
||||
"rename": "Rename"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"translation": {
|
||||
"all.files": "Tous les fichiers",
|
||||
"favorites": "Favoris",
|
||||
"no.content": "Aucun contenu à afficher",
|
||||
"enter.file.name": "Entrez le nom du fichier",
|
||||
"modify.file.name": "Modifier le nom du fichier",
|
||||
"you.are.going.to.erase.file.folder": "Vous allez supprimer le fichier/dossier",
|
||||
"apply.to.all.*": "Appliquer à tous*",
|
||||
"*.text": "* Vous ne pouvez pas annuler cette action",
|
||||
"create.new.file": "Créer un nouveau fichier",
|
||||
"name.of.file": "Nom du fichier",
|
||||
"change.file.name": "Changer le nom du fichier",
|
||||
"file.already.exist": "Le fichier existe déjà",
|
||||
"file.pt.1": "Le fichier \"",
|
||||
"file.pt.2": "\" existe déjà, que voulez-vous faire ?",
|
||||
"cant.rename": "Vous ne pouvez pas renommer le fichier/dossier : ",
|
||||
"cant.create.folder": "Vous ne pouvez pas créer le dossier : ",
|
||||
"already.exists": ", car il existe déjà.",
|
||||
"name": "Nom",
|
||||
"size": "Taille",
|
||||
"type": "Type",
|
||||
"options": "Options",
|
||||
"new": "Nouveau",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Editer",
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Confirmer",
|
||||
"create": "Créer",
|
||||
"overwrite": "Écraser",
|
||||
"rename": "Renommer"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"translation": {
|
||||
"all.files" : "Alle Dateien",
|
||||
"favorites": "Favoriten",
|
||||
"no.content": "Kein Inhalt zum Anzeigen",
|
||||
"enter.file.name": "Geben Sie den Namen der Datei ein",
|
||||
"modify.file.name": "Ändern Sie den Namen der Datei",
|
||||
"you.are.going.to.erase.file.folder" : "Sie sind dabei, die Datei/den Ordner zu löschen",
|
||||
"apply.to.all.*" : "Auf alle anwenden*",
|
||||
"*.text" : "* Diese Aktion kann nicht rückgängig gemacht werden",
|
||||
"create.new.file": "Neue Datei erstellen",
|
||||
"name.of.file": "Name der Datei",
|
||||
"change.file.name": "Ändern Sie den Namen der Datei",
|
||||
"file.already.exist" : "Die Datei existiert bereits",
|
||||
"file.pt.1" : "Die Datei \"",
|
||||
"file.pt.2" : "\" existiert bereits, was möchten Sie tun?",
|
||||
"cant.rename": "Die Datei/der Ordner kann nicht umbenannt werden: ",
|
||||
"cant.create.folder": "Der Ordner kann nicht erstellt werden: ",
|
||||
"already.exists": ", da er bereits existiert.",
|
||||
"name": "Name",
|
||||
"size": "Größe",
|
||||
"type": "Typ",
|
||||
"options": "Optionen",
|
||||
"new": "Neu",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": "Bestätigen",
|
||||
"create": "Erstellen",
|
||||
"overwrite": "Überschreiben",
|
||||
"rename": "Umbenennen"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"translation": {
|
||||
"all.files": "Todos los archivos",
|
||||
"favorites": "Favoritos",
|
||||
"no.content": "No hay contenido para mostrar",
|
||||
"enter.file.name": "Introduce el nombre del archivo",
|
||||
"modify.file.name": "Modificar el nombre del archivo",
|
||||
"you.are.going.to.erase.file.folder": "Vas a eliminar el archivo/carpeta",
|
||||
"apply.to.all.*": "Aplicar a todos*",
|
||||
"*.text": "* No puedes deshacer esta acción",
|
||||
"create.new.file": "Crear un nuevo archivo",
|
||||
"name.of.file": "Nombre del archivo",
|
||||
"change.file.name": "Cambiar el nombre del archivo",
|
||||
"file.already.exist": "El archivo ya existe",
|
||||
"file.pt.1": "El archivo \"",
|
||||
"file.pt.2": "\" ya existe, ¿qué quieres hacer?",
|
||||
"cant.rename": "No puedes renombrar el archivo/carpeta: ",
|
||||
"cant.create.folder": "No puedes crear la carpeta: ",
|
||||
"already.exists": ", porque ya existe.",
|
||||
"name": "Nombre",
|
||||
"size": "Tamaño",
|
||||
"type": "Tipo",
|
||||
"options": "Opciones",
|
||||
"new": "Nuevo",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"create": "Crear",
|
||||
"overwrite": "Sobrescribir",
|
||||
"rename": "Renombrar"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"translation": {
|
||||
"all.files" : "Todos os arquivos",
|
||||
"favorites": "Favoritos",
|
||||
"no.content": "Nenhum conteúdo para exibir",
|
||||
"enter.file.name": "Digite o nome do arquivo",
|
||||
"modify.file.name": "Modificar o nome do arquivo",
|
||||
"you.are.going.to.erase.file.folder" : "Você está prestes a apagar o arquivo/pasta",
|
||||
"apply.to.all.*" : "Aplicar a todos*",
|
||||
"*.text" : "* Esta ação não pode ser desfeita",
|
||||
"create.new.file": "Criar um novo arquivo",
|
||||
"name.of.file": "Nome do arquivo",
|
||||
"change.file.name": "Alterar o nome do arquivo",
|
||||
"file.already.exist" : "O arquivo já existe",
|
||||
"file.pt.1" : "O arquivo \"",
|
||||
"file.pt.2" : "\" já existe, o que você deseja fazer?",
|
||||
"cant.rename": "Não é possível renomear o arquivo/pasta: ",
|
||||
"cant.create.folder": "Não é possível criar a pasta: ",
|
||||
"already.exists": ", porque já existe.",
|
||||
"name": "Nome",
|
||||
"size": "Tamanho",
|
||||
"type": "Tipo",
|
||||
"options": "Opções",
|
||||
"new": "Novo",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"create": "Criar",
|
||||
"overwrite": "Sobrescrever",
|
||||
"rename": "Renomear"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"translation": {
|
||||
"all.files" : "Tutti i file",
|
||||
"favorites": "Preferiti",
|
||||
"no.content": "Nessun contenuto da visualizzare",
|
||||
"enter.file.name": "Inserisci il nome del file",
|
||||
"modify.file.name": "Modifica il nome del file",
|
||||
"you.are.going.to.erase.file.folder" : "Stai per cancellare il file/la cartella",
|
||||
"apply.to.all.*" : "Applica a tutti*",
|
||||
"*.text" : "* Non puoi annullare questa azione",
|
||||
"create.new.file": "Crea un nuovo file",
|
||||
"name.of.file": "Nome del file",
|
||||
"change.file.name": "Cambia il nome del file",
|
||||
"file.already.exist" : "Il file esiste già",
|
||||
"file.pt.1" : "Il file \"",
|
||||
"file.pt.2" : "\" esiste già, cosa vuoi fare?",
|
||||
"cant.rename": "Non puoi rinominare il file/la cartella: ",
|
||||
"cant.create.folder": "Non puoi creare la cartella: ",
|
||||
"already.exists": ", perché esiste già.",
|
||||
"name": "Nome",
|
||||
"size": "Dimensione",
|
||||
"type": "Tipo",
|
||||
"options": "Opzioni",
|
||||
"new": "Nuovo",
|
||||
"delete": "Elimina",
|
||||
"edit": "Modifica",
|
||||
"cancel": "Annulla",
|
||||
"confirm": "Conferma",
|
||||
"create": "Crea",
|
||||
"overwrite": "Sovrascrivi",
|
||||
"rename": "Rinomina"
|
||||
}
|
||||
}
|
||||
}
|
117
src/components/EditFileName.vue
Normal file
117
src/components/EditFileName.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 flex items-center justify-center bg-gray-700 bg-opacity-50 z-50" @click="closeModal">
|
||||
<div class="dark:bg-NcBlack bg-white rounded-lg shadow-lg p-6 w-96" @click.stop>
|
||||
<h2 class="text-lg font-semibold mb-4">{{ translate('modify.file.name') }}</h2>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newFileName"
|
||||
@input="onInputChange"
|
||||
@keyup.enter="save"
|
||||
placeholder="Entrez le nom du fichier"
|
||||
class="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="save" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('rename') }}</button>
|
||||
<button @click="closeModal" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FileNameEditor",
|
||||
props: {
|
||||
initialFileName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDirectory:{
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
translate: {
|
||||
type: Function,
|
||||
Required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let newFileName = this.initialFileName;
|
||||
let extension = '';
|
||||
let nbParts = 1;
|
||||
if(!this.isDirectory) {
|
||||
let nameSplit = newFileName.split('.');
|
||||
nbParts = nameSplit.length;
|
||||
if (nameSplit.length > 1) {
|
||||
extension = nameSplit.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newFileName,
|
||||
extension,
|
||||
nbParts
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
if(this.newFileName !== ''){
|
||||
// Séparer le nom de fichier sans l'extension
|
||||
const fileNameWithoutExtension = this.newFileName.slice(0, this.newFileName.lastIndexOf('.'));
|
||||
const fileNameWithoutPoints = fileNameWithoutExtension.replace(/\./g, "");
|
||||
if (fileNameWithoutPoints !== '') {
|
||||
// Re-construire le nom du fichier avec l'extension d'origine
|
||||
const newFileNameWithOriginalExtension = fileNameWithoutExtension + '.' + this.extension;
|
||||
|
||||
if (!this.isDirectory && this.newFileName !== newFileNameWithOriginalExtension) {
|
||||
// L'extension a été modifiée, on rétablit l'extension correcte
|
||||
this.newFileName = newFileNameWithOriginalExtension;
|
||||
}
|
||||
|
||||
this.$emit("update", { initialFileName: this.initialFileName, newFileName: this.newFileName });
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit("close");
|
||||
},
|
||||
onInputChange() {
|
||||
if (!this.isDirectory) {
|
||||
this.newFileName = this.removeExtensionSurplus(this.newFileName);
|
||||
let lastIndex = this.newFileName.lastIndexOf('.');
|
||||
let fileNameWithoutExtension;
|
||||
if(lastIndex != -1) {
|
||||
fileNameWithoutExtension = this.newFileName.slice(0, lastIndex);
|
||||
}
|
||||
else {
|
||||
fileNameWithoutExtension = this.newFileName.slice(0);
|
||||
}
|
||||
|
||||
const newFileNameWithOriginalExtension = fileNameWithoutExtension + '.' + this.extension;
|
||||
|
||||
// Si l'extension est différente de celle d'origine, on la rétablit
|
||||
if (this.extension !== '' && this.newFileName !== newFileNameWithOriginalExtension) {
|
||||
// Vous pouvez ici vérifier si l'extension a été modifiée et la rétablir
|
||||
this.newFileName = newFileNameWithOriginalExtension;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeExtensionSurplus(name){
|
||||
let splitName = name.split('.');
|
||||
|
||||
if(this.nbParts != splitName.length) {
|
||||
let lenExtension = this.extension.length;
|
||||
return name.slice(0, name.length - lenExtension);
|
||||
}
|
||||
else{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
128
src/components/FileExistsDialog.vue
Normal file
128
src/components/FileExistsDialog.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 flex items-center justify-center bg-gray-700 bg-opacity-50 z-50">
|
||||
<div v-if="!displayRename && !displayOverwrite" class="dark:bg-NcBlack bg-white rounded-lg shadow-lg p-6 w-96">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ translate('file.already.exist') }}</h2>
|
||||
<p>{{ translate('file.pt.1') }}{{ fileName }}{{ translate('file.pt.2') }}</p>
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="toggleOverwrite">{{ translate('overwrite') }}</button>
|
||||
<button v-if="!isDirectory" @click="toggleRename">{{ translate('rename') }}</button>
|
||||
<button @click="onCancel">{{ translate('cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Renommer le fichier -->
|
||||
<div v-if="displayRename" class="dark:bg-NcBlack bg-white rounded-lg shadow-lg p-6 w-96">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ translate('change.file.name') }}</h2>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newFileName"
|
||||
@input="onInputChange"
|
||||
@keyup.enter="save"
|
||||
:placeholder="translate('enter.file.name')"
|
||||
class="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="save" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('confirm') }}</button>
|
||||
<button @click="toggleRename" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appliquer l'ecrasement a tous -->
|
||||
<div v-if="displayOverwrite" class="dark:bg-NcBlack bg-white rounded-lg shadow-lg p-6 w-96">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ translate('you.are.going.to.erase.file.folder') }}</h2>
|
||||
<div class="flex items-center content-evenly">
|
||||
<input type="checkbox" v-model="forAll" />
|
||||
<p>{{ translate('apply.to.all.*') }}</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400">{{ translate('*.text') }}</p>
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="onOverwrite" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('confirm') }}</button>
|
||||
<button @click="toggleOverwrite" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">{{ translate('cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isDirectory:{
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
translate: {
|
||||
type: Function,
|
||||
Required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
var newFileName = this.fileName;
|
||||
var extension = '';
|
||||
if(!this.isDirectory) {
|
||||
let nameSplit = newFileName.split('.');
|
||||
if (nameSplit.length > 1) {
|
||||
extension = nameSplit.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayRename: false,
|
||||
displayOverwrite: false,
|
||||
forAll: false,
|
||||
newFileName,
|
||||
extension,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOverwrite() {
|
||||
this.$emit('overwrite', {forAll : this.forAll});
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
toggleRename() {
|
||||
this.displayRename = !this.displayRename;
|
||||
},
|
||||
toggleOverwrite(){
|
||||
this.displayOverwrite = !this.displayOverwrite;
|
||||
},
|
||||
save() {
|
||||
if(this.newFileName !== ''){
|
||||
// Séparer le nom de fichier sans l'extension
|
||||
const fileNameWithoutExtension = this.newFileName.slice(0, this.newFileName.lastIndexOf('.'));
|
||||
const fileNameWithoutPoints = fileNameWithoutExtension.replace(/\./g, "");
|
||||
if(fileNameWithoutPoints !== '' ) {
|
||||
// Re-construire le nom du fichier avec l'extension d'origine
|
||||
const newFileNameWithOriginalExtension = fileNameWithoutExtension + '.' + this.extension;
|
||||
|
||||
if (!this.isDirectory && this.newFileName !== newFileNameWithOriginalExtension) {
|
||||
// L'extension a été modifiée, on rétablit l'extension correcte
|
||||
this.newFileName = newFileNameWithOriginalExtension;
|
||||
}
|
||||
|
||||
this.$emit("rename", { newFileName: this.newFileName });
|
||||
}
|
||||
}
|
||||
},
|
||||
onInputChange() {
|
||||
if (!this.isDirectory) {
|
||||
const fileNameWithoutExtension = this.newFileName.slice(0, this.newFileName.lastIndexOf('.'));
|
||||
const newFileNameWithOriginalExtension = fileNameWithoutExtension + '.' + this.extension;
|
||||
|
||||
// Si l'extension est différente de celle d'origine, on la rétablit
|
||||
if (this.extension !== '' && this.newFileName !== newFileNameWithOriginalExtension) {
|
||||
// Vous pouvez ici vérifier si l'extension a été modifiée et la rétablir
|
||||
|
||||
this.newFileName = newFileNameWithOriginalExtension;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
667
src/components/FileTable.vue
Normal file
667
src/components/FileTable.vue
Normal file
@ -0,0 +1,667 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<!-- Boutons pour fichiers :thumbsup: -->
|
||||
<div class="flex flex-row gap-2 p-2">
|
||||
<button
|
||||
:class="getClassButton('default')"
|
||||
@click="changeTab('default')"
|
||||
>
|
||||
{{ translate('all.files') }}
|
||||
</button>
|
||||
<button
|
||||
:class="getClassButton('favorites')"
|
||||
@click="changeTab('favorites')"
|
||||
>
|
||||
{{ translate('favorites') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col h-full w-full border">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex flex-row mt-1 ml-3 items-start container">
|
||||
<NcBreadcrumbs class="max-h-8">
|
||||
<NcBreadcrumb name="Home" title="Title of the Home folder" @click="handleClickBreadcrumb(-1)">
|
||||
</NcBreadcrumb>
|
||||
<NcBreadcrumb v-if="getBreadcrumbParts().length > 0" v-for="(part, index) in breadcrumbParts"
|
||||
:key="index" :name="part" @click="handleClickBreadcrumb(index)">
|
||||
</NcBreadcrumb>
|
||||
<template #actions>
|
||||
<div class="flex items-center ml-2">
|
||||
<button v-if="!isTransfering" @click="toggleAddFilePopup" :disabled="currentTab === 'favorites' && current_dir === '/'"
|
||||
class="flex items-center space-x-2 bg-blue-100 text-blue-600 font-medium px-4 py-2 rounded-md hover:bg-blue-200 transition">
|
||||
<Plus :size="20" />
|
||||
<span>{{translate('new')}}</span>
|
||||
</button>
|
||||
<div v-else>
|
||||
<ProgressBar :value="transferProgress" :color="transferStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NcBreadcrumbs>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Popup pour la création de fichier -->
|
||||
<div v-if="isAddFilePopupVisible"
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-700 bg-opacity-50 z-50">
|
||||
<div class="dark:bg-NcBlack bg-white rounded-lg shadow-lg p-6 w-96">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ translate('create.new.file') }}</h2>
|
||||
<input v-model="newFileName" type="text" :placeholder="translate('name.of.file')"
|
||||
class="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="createNewFile"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
|
||||
{{translate('create')}}
|
||||
</button>
|
||||
<button @click="toggleAddFilePopup"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">
|
||||
{{translate('cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="flex h-12 items-center border-b border-gray-300">
|
||||
<div class="w-7/12 px-4 py-2 text-gray-500 font-semibold border-r border-gray-300">{{ translate('name') }}</div>
|
||||
<div class="w-2/12 px-4 py-2 text-gray-500 font-semibold border-r border-gray-300">{{ translate('type') }}</div>
|
||||
<div class="w-2/12 px-4 py-2 text-gray-500 font-semibold">{{ translate('size') }}</div>
|
||||
<div class="w-1/12 px-4 py-2 text-gray-500 font-semibold">{{ translate('options') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div :class="[
|
||||
'overflow-y-auto h-full mb-14 rounded-xl',
|
||||
isDragging && isDroppable ? 'border-green-500 border-4 border-dashed transition-all ease-in-out' :
|
||||
isDragging && !isDroppable ? 'border-red-500 border-4 border-dashed transition-all ease-in-out !cursor-no-drop' : ''
|
||||
]" @drop.prevent="onDrop" @dragover.prevent="onDragOver" @dragenter.prevent="onDragEnter"
|
||||
@dragleave.prevent="onDragLeave($event)" @dragend="onDragEnd">
|
||||
|
||||
<div v-for="file in files" :key="file.filename"
|
||||
class="flex h-16 items-center dark:hover:bg-NcGray hover:bg-NcWhite rounded-lg border-b last:border-b-0 border-gray-300 cursor-pointer"
|
||||
@click="handleClickElem(file)">
|
||||
|
||||
<!-- Nom -->
|
||||
<div class="w-7/12 flex items-center px-4 py-2 border-r border-gray-300 cursor-pointer">
|
||||
<div class="w-12 h-12 flex items-center justify-cente cursor-pointer">
|
||||
<template v-if="file.type === 'directory'">
|
||||
<svg fill="currentColor" viewBox="0 0 24 24" class="text-NcBlue w-10 h-10 ">
|
||||
<path
|
||||
d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-if="file.type === 'file' && file.basename.split('.').pop() !== 'zip'">
|
||||
<div :class="['flex items-center justify-center cursor-pointer']">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"
|
||||
class="w-10 h-10"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||
<path
|
||||
d="M6 22c-.55 0-1.021-.196-1.412-.587A1.927 1.927 0 0 1 4 20V4c0-.55.196-1.021.588-1.413A1.926 1.926 0 0 1 6 2h8l6 6v12a1.93 1.93 0 0 1-.587 1.413A1.93 1.93 0 0 1 18 22H6Z"
|
||||
style="fill:#969696;fill-rule:nonzero"
|
||||
transform="matrix(.7 0 0 .7 -.43 -.388)" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="file.type === 'file' && file.basename.split('.').pop() === 'zip'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-10 h-10 ">
|
||||
<path fill="#969696"
|
||||
d="M5.12,5H18.87L17.93,4H5.93L5.12,5M20.54,5.23C20.83,5.57 21,6 21,6.5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6.5C3,6 3.17,5.57 3.46,5.23L4.84,3.55C5.12,3.21 5.53,3 6,3H18C18.47,3 18.88,3.21 19.15,3.55L20.54,5.23M6,18H12V15H6V18Z" />
|
||||
</svg>
|
||||
</template>
|
||||
</div>
|
||||
<div class="ml-4 cursor-pointer max-sm:max-w-32 truncate">{{ file.basename }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div class="w-2/12 px-4 py-2 border-r border-gray-300 cursor-pointer">
|
||||
{{ file.type === 'directory' ? 'Dossier' : 'Fichier' }}
|
||||
</div>
|
||||
|
||||
<!-- Taille -->
|
||||
<div class="w-2/12 px-4 py-2 cursor-pointer">
|
||||
{{ file.type === 'directory' ? '-' : formatFileSize(file.size) }}
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="w-1/12 px-4 py-2" @click.stop>
|
||||
<NcActions>
|
||||
<NcActionButton @click="deleteElem(file)" :closeAfterClick="true">
|
||||
<template #icon>
|
||||
<Delete :size="20" />
|
||||
</template>
|
||||
{{ translate('delete') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton @click="editElem(file)" :closeAfterClick="true">
|
||||
<template #icon>
|
||||
<Pencil :size="20" />
|
||||
</template>
|
||||
{{ translate('edit') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditFileName v-if="!editDialogDisabled" :initialFileName="initialFileName" :isDirectory="isDirectory" :translate="translate"
|
||||
@update="updateFileName" @close="closeEditDialog">
|
||||
</EditFileName>
|
||||
<FileExistsDialog v-if="!fileExistDialogDisabled" :fileName="initialFileName" :isDirectory="isDirectory" :translate="translate"
|
||||
@overwrite="setOverwrite" @rename="setRename" @cancel="cancelDrop">
|
||||
</FileExistsDialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// NextCloud Components
|
||||
import { getClient, getRootPath, getFavoriteNodes } from '@nextcloud/files/dav';
|
||||
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js';
|
||||
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js';
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js';
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js';
|
||||
|
||||
// Custom components
|
||||
import ProgressBar from './ProgressBar.vue';
|
||||
import EditFileName from './EditFileName.vue';
|
||||
import FileExistsDialog from './FileExistsDialog.vue';
|
||||
|
||||
// Icons
|
||||
import Plus from 'vue-material-design-icons/Plus.vue'
|
||||
import Delete from 'vue-material-design-icons/Delete.vue';
|
||||
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
||||
|
||||
export default {
|
||||
name: 'FileTable',
|
||||
components: {
|
||||
NcBreadcrumbs,
|
||||
NcBreadcrumb,
|
||||
Plus,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
ProgressBar,
|
||||
Delete,
|
||||
Pencil,
|
||||
EditFileName,
|
||||
FileExistsDialog
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
dragEnded: {
|
||||
type: Boolean,
|
||||
Required: true,
|
||||
},
|
||||
translate: {
|
||||
type: Function,
|
||||
Required: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dragEnded(val) {
|
||||
if(val === true) {
|
||||
this.isDragging = false;
|
||||
this.isDroppable = false;
|
||||
this.$emit('dragEnded');
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
trad: null,
|
||||
files: [], // Liste des fichiers et dossiers récupérés
|
||||
root_path: getRootPath(),
|
||||
current_dir: '/',
|
||||
breadcrumbParts: [],
|
||||
isAddFilePopupVisible: false,
|
||||
newFileName: '',
|
||||
isTransfering: false,
|
||||
isDragging: false,
|
||||
isDroppable: true,
|
||||
editDialogDisabled: true,
|
||||
fileExistDialogDisabled: true,
|
||||
initialFileName: '', // Nom originel du fichier/dossier a edite
|
||||
isDirectory: false, // Si l'element a edite est un dossier ou non
|
||||
transferProgress: 0,
|
||||
transferStatus: 'bg-blue-500',
|
||||
overwrite: false,
|
||||
applyToAll: false,
|
||||
cancelOperation: false,
|
||||
rename: false,
|
||||
newElemName: '',
|
||||
currentTab: 'default',
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchFiles();
|
||||
this.breadcrumbParts = this.getBreadcrumbParts();
|
||||
},
|
||||
methods: {
|
||||
async changeTab(name) {
|
||||
this.currentTab = name;
|
||||
this.current_dir = '/';
|
||||
await this.fetchFiles();
|
||||
if(this.currentTab === 'default'){
|
||||
this.isDroppable = true;
|
||||
}
|
||||
else {
|
||||
this.isDroppable = false;
|
||||
}
|
||||
},
|
||||
async fetchFiles() {
|
||||
try {
|
||||
const client = getClient();
|
||||
let directoryItems;
|
||||
if (this.currentTab === 'default' || (this.currentTab === 'favorites' && this.current_dir !== '/')){
|
||||
directoryItems = await client.getDirectoryContents(this.root_path + this.current_dir);
|
||||
}
|
||||
else if(this.currentTab === 'favorites'){
|
||||
let favoriteNodes = await getFavoriteNodes(client);
|
||||
directoryItems = this.computeFavoritesNodes(favoriteNodes);
|
||||
}
|
||||
|
||||
this.files = directoryItems.map(file => ({
|
||||
basename: file.basename,
|
||||
filename:file.filename,
|
||||
size: file.size,
|
||||
href: client.getFileDownloadLink(file.filename),
|
||||
type: file.type
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des fichiers et dossiers :', error);
|
||||
}
|
||||
},
|
||||
computeFavoritesNodes(favoriteNodes) {
|
||||
let directoryItems = [];
|
||||
|
||||
let i = 0;
|
||||
favoriteNodes.forEach(element => {
|
||||
// Création de l'objet elemData pour chaque élément
|
||||
let elemData = {
|
||||
basename: element._data.displayname,
|
||||
etag: element._attributes.etag,
|
||||
filename: element._attributes.filename,
|
||||
lastmod: element._attributes.lastmod,
|
||||
mime: element._data.mime,
|
||||
size: element._data.size,
|
||||
type: element._attributes.type,
|
||||
};
|
||||
|
||||
// Ajout de elemData à directoryItems, indexé par un identifiant unique (par exemple, basename)
|
||||
directoryItems[i] = elemData;
|
||||
i++;
|
||||
});
|
||||
|
||||
return directoryItems;
|
||||
},
|
||||
formatFileSize(size) {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`;
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
},
|
||||
generateCrumbHref(index) {
|
||||
const parts = this.breadcrumbParts.slice(0, index + 1);
|
||||
return '/' + parts.join('/');
|
||||
},
|
||||
getBreadcrumbParts() {
|
||||
// Si le current_dir est un simple '/', on le renvoie sous forme de tableau vide.
|
||||
if (this.current_dir === '/') return [];
|
||||
return this.current_dir.split('/').filter(part => part);
|
||||
},
|
||||
async handleClickElem(file) {
|
||||
if (this.isTransfering) return;
|
||||
if (file.type === 'directory') {
|
||||
if(this.currentTab === 'default'){
|
||||
this.current_dir = this.current_dir === '/' ? '/' + file.basename : this.current_dir + '/' + file.basename;
|
||||
}
|
||||
else{
|
||||
let path = file.filename;
|
||||
let pathSplited = path.split('/');
|
||||
let result = pathSplited.slice(3);
|
||||
let dir = '';
|
||||
result.forEach(element => {
|
||||
dir += '/' + element
|
||||
});
|
||||
this.current_dir = dir;
|
||||
}
|
||||
this.breadcrumbParts = this.getBreadcrumbParts()
|
||||
await this.fetchFiles();
|
||||
} else {
|
||||
window.open(file.href, '_blank');
|
||||
}
|
||||
},
|
||||
async handleClickBreadcrumb(index) {
|
||||
if (this.isTransfering) return;
|
||||
let dir = '/';
|
||||
if (index >= -1) {
|
||||
dir = this.generateCrumbHref(index);
|
||||
}
|
||||
this.current_dir = dir;
|
||||
this.breadcrumbParts = this.getBreadcrumbParts();
|
||||
await this.fetchFiles();
|
||||
},
|
||||
async createNewFile() {
|
||||
if (!this.newFileName) return;
|
||||
|
||||
try {
|
||||
const client = getClient();
|
||||
let filePath = '';
|
||||
if (this.current_dir[this.current_dir.length - 1] === '/') {
|
||||
filePath = `${this.root_path}${this.current_dir}${this.newFileName}`;
|
||||
}
|
||||
else {
|
||||
filePath = `${this.root_path}${this.current_dir}/${this.newFileName}`;
|
||||
}
|
||||
const alreadyExists = await this.elemtAlreadyExists(filePath);
|
||||
if (!alreadyExists) {
|
||||
await client.createDirectory(filePath, '');
|
||||
this.newFileName = '';
|
||||
this.isAddFilePopupVisible = false;
|
||||
await this.fetchFiles();
|
||||
}
|
||||
else {
|
||||
alert(this.translate("cant.create.folder") + this.newFileName + this.translate('already.exists'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du fichier :', error);
|
||||
}
|
||||
},
|
||||
toggleAddFilePopup() {
|
||||
this.isAddFilePopupVisible = !this.isAddFilePopupVisible;
|
||||
if (!this.isAddFilePopupVisible) this.newFileName = '';
|
||||
},
|
||||
onDragOver(event) {
|
||||
event.preventDefault();
|
||||
if(this.currentTab === 'favorites' && this.current_dir === '/'){
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
this.isDroppable = false;
|
||||
}
|
||||
else{
|
||||
this.isDroppable = true;
|
||||
}
|
||||
if (!this.isDragging) {
|
||||
this.isDragging = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
onDragEnter(event){
|
||||
event.preventDefault()
|
||||
},
|
||||
onDragLeave(event) {
|
||||
event.preventDefault();
|
||||
if (event.target === event.currentTarget) {
|
||||
this.isDragging = false;
|
||||
}
|
||||
},
|
||||
onDragEnd() {
|
||||
this.isDragging = false;
|
||||
},
|
||||
async onDrop(event) {
|
||||
event.preventDefault();
|
||||
this.isDragging = false; // Pour enlever le contour rouge si on ne peut pas drop sinon il reste affiche
|
||||
if(this.isDroppable){
|
||||
try {
|
||||
this.isTransfering = true;
|
||||
const file = this.file;
|
||||
if (!file) return;
|
||||
|
||||
if (file.isList) {
|
||||
await this.moveListOfFiles(file);
|
||||
} else {
|
||||
if (file.isDirectory) {
|
||||
await this.moveFilesOfFolder(file, '');
|
||||
} else {
|
||||
this.transferProgress = 25;
|
||||
if (file.content && typeof file.content.arrayBuffer === 'function') {
|
||||
file.content = await file.content.arrayBuffer();
|
||||
}
|
||||
this.transferProgress = 50;
|
||||
await this.moveFileToTarget(file, '');
|
||||
this.transferProgress = 100;
|
||||
}
|
||||
}
|
||||
|
||||
this.isTransfering = false;
|
||||
this.transferProgress = 0;
|
||||
this.cancelOperation = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du drop :', error);
|
||||
this.transferStatus = 'bg-red-500';
|
||||
this.isTransfering = false;
|
||||
}
|
||||
this.overwrite = false;
|
||||
this.applyToAll = false;
|
||||
this.rename = false;
|
||||
this.newElemName = '';
|
||||
}
|
||||
this.isDroppable = true;
|
||||
},
|
||||
async moveListOfFiles(files) {
|
||||
for (const file of files.children) {
|
||||
this.transferProgress += 100 / files.children.length;
|
||||
if (file.isDirectory) {
|
||||
await this.moveFilesOfFolder(file, file.parentPath + '/');
|
||||
} else {
|
||||
if (file.content && typeof file.content.arrayBuffer === 'function') {
|
||||
file.content = await file.content.arrayBuffer();
|
||||
}
|
||||
await this.moveFileToTarget(file, '');
|
||||
}
|
||||
}
|
||||
},
|
||||
async moveFilesOfFolder(folder, parentPath) {
|
||||
await this.createFolder(folder, parentPath + '/');
|
||||
const checkChildrenInChildren = (folder) => {
|
||||
let total = folder.children.length;
|
||||
for (const child of folder.children) {
|
||||
if (child.isDirectory) {
|
||||
total += checkChildrenInChildren(child);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
const progressSteps = Math.floor(100 / checkChildrenInChildren(folder));
|
||||
|
||||
for (const child of folder.children) {
|
||||
if(!this.cancelOperation){
|
||||
this.transferProgress += progressSteps;
|
||||
if (child.isDirectory) {
|
||||
await this.moveFilesOfFolder(child, parentPath + '/' + child.parentPath + '/');
|
||||
} else {
|
||||
if (child.content && typeof child.content.arrayBuffer === 'function') {
|
||||
child.content = await child.content.arrayBuffer();
|
||||
}
|
||||
await this.moveFileToTarget(child, parentPath + '/' + child.parentPath + '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async moveFileToTarget(file, parentPath, newName = null) {
|
||||
this.isDirectory = false;
|
||||
try {
|
||||
const client = getClient();
|
||||
// Assurez-vous que le chemin parent est correctement formaté
|
||||
|
||||
let fullPath = '';
|
||||
if(!this.rename) {
|
||||
fullPath = `${this.root_path}${this.current_dir}${parentPath}/${file.name}`;
|
||||
}
|
||||
else if (this.rename && newName){
|
||||
fullPath = `${this.root_path}${this.current_dir}${parentPath}${newName}`;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(file.content)) {
|
||||
file.content = Buffer.from(file.content);
|
||||
}
|
||||
|
||||
const alreadyExists = await this.elemtAlreadyExists(fullPath);
|
||||
if(!alreadyExists || this.overwrite) {
|
||||
// Évitez les chemins incorrects en utilisant `path.normalize` si disponible
|
||||
await client.putFileContents(fullPath, file.content);
|
||||
|
||||
if (this.overwrite && !this.applyToAll) {
|
||||
this.overwrite = false;
|
||||
}
|
||||
|
||||
// Recharge les fichiers après l'opération
|
||||
await this.fetchFiles();
|
||||
}
|
||||
else {
|
||||
this.initialFileName = file.name;
|
||||
this.fileExistDialogDisabled = false;
|
||||
while (!this.fileExistDialogDisabled) {
|
||||
await this.sleep(50);
|
||||
}
|
||||
if(!this.cancelOperation){
|
||||
if(this.rename) {
|
||||
await this.moveFileToTarget(file, parentPath, this.newElemName);
|
||||
this.rename = false;
|
||||
}
|
||||
else{
|
||||
await this.moveFileToTarget(file,parentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du déplacement du fichier:', error);
|
||||
}
|
||||
},
|
||||
async createFolder(folder, parentPath) {
|
||||
this.isDirectory = true;
|
||||
try {
|
||||
const client = getClient();
|
||||
let fullPath = '';
|
||||
fullPath = `${this.root_path}${this.current_dir}${parentPath}${folder.name}`;
|
||||
|
||||
const alreadyExists = await this.elemtAlreadyExists(fullPath);
|
||||
if (!alreadyExists) {
|
||||
await client.createDirectory(fullPath);
|
||||
await this.fetchFiles();
|
||||
}
|
||||
else if(!this.applyToAll){
|
||||
this.initialFileName = folder.name;
|
||||
this.fileExistDialogDisabled = false;
|
||||
while (!this.fileExistDialogDisabled) {
|
||||
await this.sleep();
|
||||
}
|
||||
if(this.overwrite && !this.applyToAll) {
|
||||
this.overwrite = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du dossier :', error);
|
||||
}
|
||||
},
|
||||
async deleteElem(file) {
|
||||
const client = getClient()
|
||||
try {
|
||||
let path = this.root_path + this.current_dir + "/" + file.basename;
|
||||
await client.deleteFile(path);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Erreur lors de la suppression d\'un element : ', error);
|
||||
}
|
||||
|
||||
await this.fetchFiles();
|
||||
},
|
||||
/**
|
||||
* Change les props pour le composant EditFileName
|
||||
* @param file le ficher/dossier dont on veut editer le nom
|
||||
*/
|
||||
async editElem(file) {
|
||||
if (file.type === 'file') {
|
||||
this.isDirectory = false;
|
||||
}
|
||||
else {
|
||||
this.isDirectory = true;
|
||||
}
|
||||
this.initialFileName = file.basename;
|
||||
this.editDialogDisabled = false;
|
||||
},
|
||||
/**
|
||||
* Ferme la fenetre d'edition du nom du fichier/dossier
|
||||
*/
|
||||
closeEditDialog() {
|
||||
this.editDialogDisabled = true;
|
||||
},
|
||||
closeFileExistsDialog() {
|
||||
this.fileExistDialogDisabled = true;
|
||||
},
|
||||
/**
|
||||
* Change le nom du fichier sur le serveur Cloud via un client WebDAV
|
||||
* @param names Contient un initialFileName et un newFileName
|
||||
*/
|
||||
async updateFileName(names) {
|
||||
if (names.initialFileName !== names.newFileName) {
|
||||
const client = getClient()
|
||||
try {
|
||||
const oldName = this.root_path + this.current_dir + '/' + names.initialFileName;
|
||||
const newName = this.root_path + this.current_dir + '/' + names.newFileName;
|
||||
let alreadyExists = await this.elemtAlreadyExists(newName);
|
||||
if (!alreadyExists) {
|
||||
await client.moveFile(oldName, newName);
|
||||
}
|
||||
else {
|
||||
alert(this.translate('cant.rename') + names.newFileName + this.translate('already.exists'));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Erreur lors du renommage d\'un element : ', error);
|
||||
}
|
||||
await this.fetchFiles();
|
||||
}
|
||||
},
|
||||
setOverwrite(options) {
|
||||
this.overwrite = true;
|
||||
this.applyToAll = options.forAll;
|
||||
this.fileExistDialogDisabled = true;
|
||||
},
|
||||
setRename(options) {
|
||||
this.rename = true;
|
||||
this.newElemName = options.newFileName;
|
||||
this.fileExistDialogDisabled = true;
|
||||
},
|
||||
/**
|
||||
* Check si un fichier ou un dossier existe deja sur le serveur
|
||||
* @param path le chemin du fichier/dossier
|
||||
*/
|
||||
async elemtAlreadyExists(path) {
|
||||
const client = getClient();
|
||||
let exists = await client.exists(path);
|
||||
|
||||
return exists;
|
||||
},
|
||||
cancelDrop(){
|
||||
this.cancelOperation = true;
|
||||
this.closeFileExistsDialog();
|
||||
},
|
||||
getClassButton(name) {
|
||||
let cssStyle;
|
||||
|
||||
if(this.currentTab === name) {
|
||||
cssStyle = ' !bg-NcBlue/50';
|
||||
} else {
|
||||
cssStyle = '';
|
||||
}
|
||||
|
||||
return cssStyle;
|
||||
},
|
||||
async sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Vous pouvez ajouter des styles personnalisés ici si nécessaire */
|
||||
</style>
|
36
src/components/ProgressBar.vue
Normal file
36
src/components/ProgressBar.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex items-center w-48 h-full">
|
||||
<div class="relative w-full h-2 bg-gray-200/10 rounded-full overflow-hidden">
|
||||
<div :style="{ width: value + '%' }" class="h-full rounded-full" :class="color"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProgressBar",
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "Progress",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "bg-blue-500",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
clampedValue() {
|
||||
return Math.max(0, Math.min(this.value, 100));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Optionnel: Ajoutez un style personnalisé ici si nécessaire */
|
||||
</style>
|
458
src/components/WebContentViewer.vue
Normal file
458
src/components/WebContentViewer.vue
Normal file
@ -0,0 +1,458 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-full border">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex flex-row mt-1 items-start container">
|
||||
<NcBreadcrumbs class="max-h-8">
|
||||
<NcBreadcrumb name="Home" title="Title of the Home folder" @click="handleClickBreadcrumb(-1)">
|
||||
</NcBreadcrumb>
|
||||
<NcBreadcrumb v-if="getBreadcrumbParts().length > 0" v-for="(part, index) in breadcrumbParts"
|
||||
:key="index" :name="part" @click="handleClickBreadcrumb(index)">
|
||||
</NcBreadcrumb>
|
||||
</NcBreadcrumbs>
|
||||
</div>
|
||||
|
||||
<div class="flex h-12 items-center border-b border-gray-300">
|
||||
<div @click.stop class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" id="checkbox-file"
|
||||
class="NIQUE TA MERE CA CHANGE RIEN PARCE QUE HTML/CSS C DE LA MERDE"
|
||||
@change="handleCheckAll($event)" v-model="checkedAll" />
|
||||
</div>
|
||||
<div class="ml-2 w-5/6 px-4 py-2 text-gray-500 font-semibold border-r border-gray-300">{{
|
||||
translate('name')}}</div>
|
||||
<div class="w-1/6 px-4 py-2 text-gray-500 font-semibold">{{ translate('size') }}</div>
|
||||
</div>
|
||||
<!-- Archive depliee -->
|
||||
<div v-if="!isLoading && zipContent.length !== 0" class="overflow-y-auto h-full">
|
||||
<div v-for="(file, index) in cachedSortedFiles" :key="file.fullPath" class="flex flex-col">
|
||||
|
||||
<div class="flex flex-row w-full gap-2">
|
||||
|
||||
<div v-if="isVisible(file)" @click.stop class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" id="checkbox-file"
|
||||
class="NIQUE TA MERE SA CHANGE RIEN PARCE QUE HTML/CSS C DE LA MERDE"
|
||||
@change="handleCheckboxChange(file, $event)" :checked="isChecked(file)" />
|
||||
</div>
|
||||
|
||||
<div class="flex w-full h-16 dark:hover:bg-NcGray hover:bg-NcWhite items-center pl-4 cursor-pointer rounded-lg border-b last:border-b-0 border-gray-300"
|
||||
@click="toggleFolder(file)" v-if="file.isDirectory && isVisible(file)" draggable="true"
|
||||
@dragstart="onDragStart(file)" @dragend="onDragEnd">
|
||||
|
||||
<div class="w-5/6 flex items-center py-2 border-r border-gray-300 cursor-pointer">
|
||||
<div class="w-12 h-12 flex items-center justify-center cursor-pointer">
|
||||
<template>
|
||||
<svg fill="currentColor" viewBox="0 0 24 24" class="text-NcBlue w-10 h-10 ">
|
||||
<path
|
||||
d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-4/6 flex items-center py-2 border-r border-gray-300 cursor-pointer">
|
||||
<span class="ml-2 truncate cursor-pointer">{{ file.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/6 px-4 py-2 cursor-pointer">-</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex h-16 w-full dark:hover:bg-NcGray hover:bg-NcWhite items-center pl-4 cursor-pointer rounded-lg border-b last:border-b-0 border-gray-300"
|
||||
v-else-if="isVisible(file)" draggable="true" @dragstart="onDragStart(file, $event)">
|
||||
<template>
|
||||
<div class="flex items-center justify-center cursor-pointer">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"
|
||||
class="w-10 h-10"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
|
||||
<path
|
||||
d="M6 22c-.55 0-1.021-.196-1.412-.587A1.927 1.927 0 0 1 4 20V4c0-.55.196-1.021.588-1.413A1.926 1.926 0 0 1 6 2h8l6 6v12a1.93 1.93 0 0 1-.587 1.413A1.93 1.93 0 0 1 18 22H6Z"
|
||||
style="fill:#969696;fill-rule:nonzero"
|
||||
transform="matrix(.7 0 0 .7 -.43 -.388)" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-5/6 flex items-center px-4 py-2 cursor-pointer">
|
||||
<div class="truncate max-sm:max-w-32 max-w-96 cursor-pointer">{{ file.name }}</div>
|
||||
</div>
|
||||
<div class="w-1/6 py-2 cursor-pointer">
|
||||
{{ formatFileSize(file.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoading" class="flex h-full items-center justify-center">
|
||||
<component :is="Loading" class="text-white w-24 h-24 animate-spin" :size="40" />
|
||||
</div>
|
||||
<div v-if="!isLoading && zipContent.length === 0" class="flex h-full items-center justify-center">
|
||||
<span class="text-gray-500">{{ translate('no.content') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js';
|
||||
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js';
|
||||
import JSZip from 'jszip';
|
||||
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue';
|
||||
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
||||
import Loading from 'vue-material-design-icons/Loading.vue';
|
||||
import { ref } from 'vue';
|
||||
import {fileTypeFromBuffer} from 'file-type';
|
||||
|
||||
export default {
|
||||
name: 'WebContentViewer',
|
||||
components: {
|
||||
NcBreadcrumbs,
|
||||
NcBreadcrumb
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
zipContent: [],
|
||||
folderMap: {},
|
||||
archiveUrl: '',
|
||||
token: '',
|
||||
ChevronRightIcon,
|
||||
ChevronDownIcon,
|
||||
isLoading: ref(false),
|
||||
Loading,
|
||||
zipName: '',
|
||||
zipSize: 0,
|
||||
currentDir: '',
|
||||
breadcrumbParts: [],
|
||||
cochedFiles: [],
|
||||
checkedAll: false,
|
||||
cachedSortedFiles: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
zipUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
translate: {
|
||||
type: Function,
|
||||
Required: true,
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
sortedFiles() {
|
||||
const flattenAndSort = (files, parentPath = '') => {
|
||||
const flatList = [];
|
||||
files.forEach(file => {
|
||||
const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name;
|
||||
|
||||
// Toujours ajouter le dossier parent
|
||||
flatList.push({
|
||||
...file,
|
||||
fullPath,
|
||||
parentPath,
|
||||
});
|
||||
|
||||
// Ajouter les enfants uniquement si le dossier est ouvert
|
||||
if (file.isDirectory && this.folderMap[fullPath] && file.children) {
|
||||
flatList.push(...flattenAndSort(file.children, fullPath));
|
||||
}
|
||||
});
|
||||
|
||||
return flatList.sort((a, b) => a.fullPath.localeCompare(b.fullPath));
|
||||
};
|
||||
|
||||
return flattenAndSort(this.zipContent);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sortedFiles: {
|
||||
handler(newVal) {
|
||||
this.cachedSortedFiles = newVal; // Met à jour la variable chaque fois que sortedFiles change
|
||||
},
|
||||
immediate: true, // Met à jour dès que le composant est monté
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.isLoading = true;
|
||||
await this.loadZipContent();
|
||||
const webTransferDiv = document.getElementById('archiveInfos');
|
||||
if (webTransferDiv) {
|
||||
this.archiveUrl = webTransferDiv.dataset.archiveUrl;
|
||||
this.token = webTransferDiv.dataset.token;
|
||||
} else {
|
||||
console.error('Pas d\'informations pour recuperer l\'archive');
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
methods: {
|
||||
async loadZipContent() {
|
||||
try {
|
||||
var baseUrl = OC.generateUrl('/apps/webtransfer/getZipFile');
|
||||
let fullUrl = baseUrl + '?subUrl=' + this.zipUrl;
|
||||
|
||||
let response = await fetch(fullUrl);
|
||||
let responseJson = await response.json();
|
||||
|
||||
const zipData = responseJson.parameters.data;
|
||||
const first10Chars = zipData.substring(0,4);
|
||||
|
||||
// Check si le debut du fichier correspond a celui d'un zip
|
||||
if(first10Chars === 'PK\x03\x04' || first10Chars === 'PK\x05\x06' || first10Chars === 'PK\x07\x08') {
|
||||
this.zipName = this.zipUrl.split('/').pop();
|
||||
const zip = await JSZip.loadAsync(zipData);
|
||||
this.zipSize = zipData.size;
|
||||
|
||||
const files = [];
|
||||
|
||||
zip.forEach((relativePath, file) => {
|
||||
const pathParts = relativePath.split('/').filter(Boolean);
|
||||
let currentLevel = files;
|
||||
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const partName = pathParts[i];
|
||||
const isDirectory = i < pathParts.length - 1 || file.dir;
|
||||
let existing = currentLevel.find(f => f.name === partName && f.isDirectory === isDirectory);
|
||||
|
||||
let promise;
|
||||
|
||||
if (!isDirectory) {
|
||||
promise = file.async("blob").then(content => {
|
||||
existing.content = content;
|
||||
});
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
existing = {
|
||||
name: pathParts[i],
|
||||
isDirectory,
|
||||
size: isDirectory ? 0 : file._data.uncompressedSize,
|
||||
content: isDirectory ? null : '', // Initialiser 'content' pour les fichiers
|
||||
children: isDirectory ? [] : null,
|
||||
depth: pathParts.length, // Profondeur du fichier dans l'arborescence
|
||||
//remove the name of the file from the path
|
||||
parentPath: i > 0 ? pathParts[i - 1] : '',
|
||||
unzip: promise
|
||||
};
|
||||
currentLevel.push(existing);
|
||||
}
|
||||
|
||||
if (isDirectory) {
|
||||
currentLevel = existing.children;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Attendre que tous les contenus de fichier soient extraits
|
||||
this.zipContent = files;
|
||||
|
||||
// Initialiser folderMap
|
||||
const initializeFolderMap = (files, parentPath = '') => {
|
||||
files.forEach(file => {
|
||||
const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name;
|
||||
this.$set(this.folderMap, fullPath, false);
|
||||
if (file.isDirectory && file.children) {
|
||||
initializeFolderMap(file.children, fullPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initializeFolderMap(this.zipContent);
|
||||
console.log('Contenu du ZIP chargé avec succès');
|
||||
}
|
||||
else{
|
||||
const uint8Array = new Uint8Array(zipData.length);
|
||||
for (let i = 0; i <zipData.length; i++) {
|
||||
uint8Array[i] = zipData.charCodeAt(i);
|
||||
}
|
||||
|
||||
try {
|
||||
let type = await fileTypeFromBuffer(uint8Array);;
|
||||
const file = new File([uint8Array], 'file.' + type.ext, {type: type.mime});
|
||||
let entry = [{
|
||||
name: file.name,
|
||||
isDirectory: false,
|
||||
size: file.size,
|
||||
content: uint8Array, // Initialiser 'content' pour les fichiers
|
||||
children: null,
|
||||
depth: 0, // Profondeur du fichier dans l'arborescence
|
||||
//remove the name of the file from the path
|
||||
parentPath: '',
|
||||
}]
|
||||
this.zipContent = entry;
|
||||
console.log('Fichier chargé avec succès');
|
||||
} catch (e) {
|
||||
console.log('Erreur lors du telechargement du fichier.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement du contenu du ZIP :', error);
|
||||
}
|
||||
},
|
||||
onDragEnd(event) {
|
||||
event.preventDefault();
|
||||
this.$emit('dragEnded');
|
||||
},
|
||||
handleCheckboxChange(file, event) {
|
||||
if (event.target.checked) {
|
||||
this.cocheFile(file);
|
||||
} else {
|
||||
this.decocheFile(file);
|
||||
}
|
||||
},
|
||||
getFullPath(file) {
|
||||
if (!file.parentPath || file.parentPath === '') {
|
||||
return file.name;
|
||||
} else {
|
||||
return `${file.parentPath}/${file.name}`;
|
||||
}
|
||||
},
|
||||
cocheFile(file) {
|
||||
if (!this.cochedFiles.some(f => this.getFullPath(f) === this.getFullPath(file))) {
|
||||
this.cochedFiles.push(file);
|
||||
}
|
||||
},
|
||||
decocheFile(file) {
|
||||
this.cochedFiles = this.cochedFiles.filter(f => this.getFullPath(f) !== this.getFullPath(file));
|
||||
},
|
||||
isChecked(file) {
|
||||
return this.cochedFiles.some(f => this.getFullPath(f) === this.getFullPath(file));
|
||||
},
|
||||
formatFileSize(size) {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`;
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
},
|
||||
toggleFolder(file) {
|
||||
if (!file.isDirectory) return;
|
||||
this.uncheckAll();
|
||||
const currentState = this.folderMap[file.fullPath];
|
||||
const parentPath = file.parentPath;
|
||||
|
||||
if (!currentState) {
|
||||
this.currentDir = file.fullPath;
|
||||
}
|
||||
else {
|
||||
this.currentDir = parentPath;
|
||||
}
|
||||
this.$set(this.folderMap, file.fullPath, !currentState);
|
||||
|
||||
if (parentPath !== '') {
|
||||
const parentState = this.folderMap[parentPath];
|
||||
this.$set(this.folderMap, parentPath, !parentState);
|
||||
}
|
||||
this.breadcrumbParts = this.getBreadcrumbParts()
|
||||
},
|
||||
async dragZip() {
|
||||
try {
|
||||
const zip = { name: this.zipName, url: this.zipUrl };
|
||||
this.$emit('zip-upload', zip);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du drag du ZIP :', error);
|
||||
}
|
||||
},
|
||||
async onDragStart(file, event) {
|
||||
const getFilesFromFolder = (folder) => {
|
||||
const files = [];
|
||||
if (!folder.children || folder.children.length === 0) return files;
|
||||
|
||||
for (let i = 0; i < folder.children.length; i++) {
|
||||
const child = folder.children[i];
|
||||
if (child.isDirectory) {
|
||||
files.push(...getFilesFromFolder(child));
|
||||
} else {
|
||||
files.push(child);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
if (this.cochedFiles.length > 0) {
|
||||
|
||||
const folder = {
|
||||
// Si des fichiers sont cochés, utiliser cette liste
|
||||
name: file.name,
|
||||
isDirectory: true,
|
||||
isList: true,
|
||||
children: this.cochedFiles,
|
||||
unzip: Promise.all(this.cochedFiles.map(file => file.unzip))
|
||||
};
|
||||
|
||||
try {
|
||||
await folder.unzip;
|
||||
this.$emit('file-upload', folder);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du drag start :', error);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Logique existante pour un seul fichier/dossier
|
||||
try {
|
||||
if (file.isDirectory) {
|
||||
const files = getFilesFromFolder(file);
|
||||
const filesToUnzip = files.map(file => file.unzip);
|
||||
await Promise.all(filesToUnzip);
|
||||
} else {
|
||||
await file.unzip;
|
||||
}
|
||||
this.$emit('file-upload', file);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du drag start :', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
isVisible(file) {
|
||||
let parentPath = file.parentPath;
|
||||
if (this.currentDir === parentPath) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getBreadcrumbParts() {
|
||||
// Si le currentDir est un simple '/', on le renvoie sous forme de tableau vide.
|
||||
if (this.currentDir === '') return [];
|
||||
return this.currentDir.split('/').filter(part => part);
|
||||
},
|
||||
generateCrumbHref(index) {
|
||||
const parts = this.breadcrumbParts.slice(0, index + 1);
|
||||
return parts.join('/');
|
||||
},
|
||||
handleClickBreadcrumb(index) {
|
||||
this.uncheckAll();
|
||||
if (this.isTransfering) return;
|
||||
let dir = '';
|
||||
if (index >= -1) {
|
||||
dir = this.generateCrumbHref(index);
|
||||
}
|
||||
this.currentDir = dir;
|
||||
this.breadcrumbParts = this.getBreadcrumbParts();
|
||||
let file = {
|
||||
fullPath: dir,
|
||||
parentPath: this.generateCrumbHref(index - 1),
|
||||
isDirectory: true,
|
||||
};
|
||||
|
||||
Object.keys(this.folderMap).forEach(key => {
|
||||
this.folderMap[key] = false;
|
||||
});
|
||||
this.toggleFolder(file)
|
||||
},
|
||||
uncheckAll() {
|
||||
this.cochedFiles = [];
|
||||
this.checkedAll = false;
|
||||
},
|
||||
handleCheckAll(event) {
|
||||
this.sortedFiles.forEach(file => {
|
||||
if(this.isVisible(file)) {
|
||||
if (event.target.checked) {
|
||||
this.cocheFile(file);
|
||||
} else {
|
||||
this.decocheFile(file);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Ajoutez ici des styles si nécessaire */
|
||||
</style>
|
3
src/input.css
Normal file
3
src/input.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
6
src/main.js
Normal file
6
src/main.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
Vue.mixin({ methods: { t, n } })
|
||||
|
||||
const View = Vue.extend(App)
|
||||
new View().$mount('#webtransfer')
|
3
stylelint.config.js
Normal file
3
stylelint.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: 'stylelint-config-recommended-vue',
|
||||
}
|
20
tailwind.config.js
Normal file
20
tailwind.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,jsx,vue}", // Fichiers dans le dossier `src`
|
||||
"./templates/**/*.{html,php}", // Fichiers dans le dossier `templates`
|
||||
],
|
||||
darkMode: ['selector', ':is([data-themes="dark"], [data-themes="dark-highcontrast"], [data-themes="default"])'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
NcBlack: '#171717',
|
||||
NcBlue: '#0072c3',
|
||||
NcGray: '#212121',
|
||||
NcWhite: '#ededed',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
18
templates/index.php
Normal file
18
templates/index.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OCP\Util;
|
||||
|
||||
Util::addScript(OCA\WebTransfer\AppInfo\Application::APP_ID, 'main');
|
||||
|
||||
$archiveUrl = isset($_['archiveUrl']) ? $_['archiveUrl'] : ''; // Valeur par défaut vide si non définie
|
||||
?>
|
||||
|
||||
<div id="webtransfer">
|
||||
</div>
|
||||
|
||||
<div id="archiveInfos"
|
||||
dataarchiveurl="<?php echo htmlspecialchars($archiveUrl); ?>"
|
||||
>
|
||||
</div>
|
9
tests/bootstrap.php
Normal file
9
tests/bootstrap.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../tests/bootstrap.php';
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
\OC_App::loadApp(OCA\WebServer\AppInfo\Application::APP_ID);
|
||||
OC_Hook::clear();
|
12
tests/phpunit.xml
Normal file
12
tests/phpunit.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="bootstrap.php" timeoutForSmallTests="900" timeoutForMediumTests="900" timeoutForLargeTests="900" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" cacheDirectory=".phpunit.cache">
|
||||
<testsuite name="Web Server Tests">
|
||||
<directory suffix="Test.php">.</directory>
|
||||
</testsuite>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">../appinfo</directory>
|
||||
<directory suffix=".php">../lib</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
19
tests/unit/Controller/ApiTest.php
Normal file
19
tests/unit/Controller/ApiTest.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use OCA\WebServer\AppInfo\Application;
|
||||
use OCA\WebServer\Controller\ApiController;
|
||||
use OCP\IRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ApiTest extends TestCase {
|
||||
public function testIndex() {
|
||||
$request = $this->createMock(IRequest::class);
|
||||
$controller = new ApiController(Application::APP_ID, $request);
|
||||
|
||||
$this->assertEquals($controller->index()->getData()['message'], 'Hello world!');
|
||||
}
|
||||
}
|
10
vendor-bin/cs-fixer/composer.json
Normal file
10
vendor-bin/cs-fixer/composer.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"require-dev": {
|
||||
"nextcloud/coding-standard": "^1.2"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1"
|
||||
}
|
||||
}
|
||||
}
|
16
vendor-bin/openapi-extractor/composer.json
Normal file
16
vendor-bin/openapi-extractor/composer.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/nextcloud/openapi-extractor"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"nextcloud/openapi-extractor": "dev-main"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1"
|
||||
}
|
||||
}
|
||||
}
|
10
vendor-bin/phpunit/composer.json
Normal file
10
vendor-bin/phpunit/composer.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1"
|
||||
}
|
||||
}
|
||||
}
|
10
vendor-bin/psalm/composer.json
Normal file
10
vendor-bin/psalm/composer.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"require-dev": {
|
||||
"vimeo/psalm": "^5.23"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1"
|
||||
}
|
||||
}
|
||||
}
|
17
webpack.js
Normal file
17
webpack.js
Normal file
@ -0,0 +1,17 @@
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||
const StyleLintPlugin = require('stylelint-webpack-plugin')
|
||||
const path = require('path')
|
||||
|
||||
webpackConfig.entry = {
|
||||
main: { import: path.join(__dirname, 'src', 'main.js'), filename: 'main.js' },
|
||||
}
|
||||
|
||||
|
||||
webpackConfig.module.rules.push({
|
||||
test: /\.svg$/i,
|
||||
type: 'asset/source',
|
||||
})
|
||||
|
||||
|
||||
module.exports = webpackConfig
|
Loading…
x
Reference in New Issue
Block a user