Compare commits

...

No commits in common. "master" and "dev" have entirely different histories.
master ... dev

56 changed files with 19817 additions and 517 deletions

9
.eslintrc.js Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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 }}

View 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
View 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

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

19
.php-cs-fixer.dist.php Normal file
View 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
View 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
View 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.

1176
LICENSE

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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
View 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
View 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
View 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

View 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 {
}
}

View 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!']
);
}
}

View 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
View 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

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View 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
View 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
View File

@ -0,0 +1 @@
/output.css

66
src/App.vue Normal file
View 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
View 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

View 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
View 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"
}
}
}

View 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>

View 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>

View 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>

View 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>

View 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
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

6
src/main.js Normal file
View 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
View File

@ -0,0 +1,3 @@
module.exports = {
extends: 'stylelint-config-recommended-vue',
}

20
tailwind.config.js Normal file
View 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
View 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
View 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
View 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>

View 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!');
}
}

View File

@ -0,0 +1,10 @@
{
"require-dev": {
"nextcloud/coding-standard": "^1.2"
},
"config": {
"platform": {
"php": "8.1"
}
}
}

View 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"
}
}
}

View File

@ -0,0 +1,10 @@
{
"require-dev": {
"phpunit/phpunit": "^10.5"
},
"config": {
"platform": {
"php": "8.1"
}
}
}

View File

@ -0,0 +1,10 @@
{
"require-dev": {
"vimeo/psalm": "^5.23"
},
"config": {
"platform": {
"php": "8.1"
}
}
}

17
webpack.js Normal file
View 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