diff --git a/frontend/package.json b/frontend/package.json index ae5609ee..7dd3c7c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@vueuse/core": "^14.0.0", "@vueuse/integrations": "^14.0.0", "ace-builds": "^1.43.2", + "csv-parse": "^6.1.0", "dayjs": "^1.11.13", "dompurify": "^3.2.6", "epubjs": "^0.3.93", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2f6cb3f8..45bc6c07 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: ace-builds: specifier: ^1.43.2 version: 1.43.6 + csv-parse: + specifier: ^6.1.0 + version: 6.1.0 dayjs: specifier: ^1.11.13 version: 1.11.19 @@ -705,24 +708,12 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -753,12 +744,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -801,24 +786,12 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -837,24 +810,12 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -945,60 +906,30 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1170,26 +1101,11 @@ packages: cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.55.1': - resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.55.1': - resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.55.1': resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.55.1': - resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.55.1': resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] @@ -1207,18 +1123,6 @@ packages: os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.55.1': - resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.55.1': - resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] @@ -1283,11 +1187,6 @@ packages: cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.55.1': - resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.55.1': resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] @@ -1854,6 +1753,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-parse@6.1.0: + resolution: {integrity: sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==} + custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} @@ -3605,15 +3507,9 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.2': - optional: true - '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.2': - optional: true - '@esbuild/android-x64@0.25.12': optional: true @@ -3629,9 +3525,6 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.2': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -3653,15 +3546,9 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.2': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.2': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true @@ -3671,15 +3558,9 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.2': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.2': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true @@ -3725,33 +3606,18 @@ snapshots: '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.2': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.2': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.2': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.2': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.2': - optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': dependencies: eslint: 9.39.2 @@ -3926,18 +3792,9 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.55.1': optional: true - '@rollup/rollup-android-arm64@4.55.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.55.1': - optional: true - '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.55.1': - optional: true - '@rollup/rollup-freebsd-x64@4.55.1': optional: true @@ -3947,12 +3804,6 @@ snapshots: '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.55.1': - optional: true - '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true @@ -3986,9 +3837,6 @@ snapshots: '@rollup/rollup-openharmony-arm64@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.55.1': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.55.1': optional: true @@ -4670,6 +4518,8 @@ snapshots: csstype@3.2.3: {} + csv-parse@6.1.0: {} + custom-error-instance@2.1.1: {} d@1.0.2: @@ -4759,19 +4609,12 @@ snapshots: esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 '@esbuild/android-x64': 0.27.2 '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 '@esbuild/freebsd-arm64': 0.27.2 '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 '@esbuild/linux-riscv64': 0.27.2 '@esbuild/linux-s390x': 0.27.2 '@esbuild/linux-x64': 0.27.2 @@ -4779,11 +4622,6 @@ snapshots: '@esbuild/netbsd-x64': 0.27.2 '@esbuild/openbsd-arm64': 0.27.2 '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} @@ -5392,15 +5230,10 @@ snapshots: '@types/estree': 1.0.8 optionalDependencies: '@rollup/rollup-android-arm-eabi': 4.55.1 - '@rollup/rollup-android-arm64': 4.55.1 - '@rollup/rollup-darwin-arm64': 4.55.1 '@rollup/rollup-darwin-x64': 4.55.1 - '@rollup/rollup-freebsd-arm64': 4.55.1 '@rollup/rollup-freebsd-x64': 4.55.1 '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 '@rollup/rollup-linux-arm-musleabihf': 4.55.1 - '@rollup/rollup-linux-arm64-gnu': 4.55.1 - '@rollup/rollup-linux-arm64-musl': 4.55.1 '@rollup/rollup-linux-loong64-gnu': 4.55.1 '@rollup/rollup-linux-loong64-musl': 4.55.1 '@rollup/rollup-linux-ppc64-gnu': 4.55.1 @@ -5412,7 +5245,6 @@ snapshots: '@rollup/rollup-linux-x64-musl': 4.55.1 '@rollup/rollup-openbsd-x64': 4.55.1 '@rollup/rollup-openharmony-arm64': 4.55.1 - '@rollup/rollup-win32-arm64-msvc': 4.55.1 '@rollup/rollup-win32-ia32-msvc': 4.55.1 '@rollup/rollup-win32-x64-gnu': 4.55.1 '@rollup/rollup-win32-x64-msvc': 4.55.1 diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts index d9ee12d6..c584d85a 100644 --- a/frontend/src/api/files.ts +++ b/frontend/src/api/files.ts @@ -3,14 +3,25 @@ import { useLayoutStore } from "@/stores/layout"; import { baseURL } from "@/utils/constants"; import { upload as postTus, useTus } from "./tus"; import { createURL, fetchURL, removePrefix, StatusError } from "./utils"; +import { isEncodableResponse, makeRawResource } from "@/utils/encodings"; export async function fetch(url: string, signal?: AbortSignal) { + const encoding = isEncodableResponse(url); url = removePrefix(url); - const res = await fetchURL(`/api/resources${url}`, { signal }); + const res = await fetchURL(`/api/resources${url}`, { + signal, + headers: { + "X-Encoding": encoding ? "true" : "false", + }, + }); let data: Resource; try { - data = (await res.json()) as Resource; + if (res.headers.get("Content-Type") == "application/octet-stream") { + data = await makeRawResource(res, url); + } else { + data = (await res.json()) as Resource; + } } catch (e) { // Check if the error is an intentional cancellation if (e instanceof Error && e.name === "AbortError") { diff --git a/frontend/src/components/files/CsvViewer.vue b/frontend/src/components/files/CsvViewer.vue index b926d390..fdcaaed9 100644 --- a/frontend/src/components/files/CsvViewer.vue +++ b/frontend/src/components/files/CsvViewer.vue @@ -4,35 +4,13 @@ error

{{ displayError }}

-
+
description

{{ $t("files.lonely") }}

- - - - - - - - - - - -
- {{ header || `Column ${index + 1}` }} -
- {{ cell }} -
- @@ -213,10 +247,6 @@ const displayError = computed(() => { padding: 0.5rem; } -.csv-footer > :only-child { - margin-left: auto; -} - .csv-info { display: flex; align-items: center; @@ -230,18 +260,25 @@ const displayError = computed(() => { font-size: 0.875rem; } -.column-separator { +.csv-header { + display: flex; + justify-content: space-between; + padding: 0.25rem; +} + +.header-select { display: flex; align-items: center; gap: 0.5rem; + margin-bottom: 0.5rem; } -.column-separator > label { +.header-select > label { font-size: small; - text-align: end; + max-width: 80px; } -.column-separator > select { +.header-select > select { margin-bottom: 0; } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3fcdf635..8938cb06 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -88,7 +88,8 @@ "comma": "Comma (,)", "semicolon": "Semicolon (;)", "both": "Both (,) and (;)" - } + }, + "fileEncoding": "File Encoding" }, "help": { "click": "select file or directory", diff --git a/frontend/src/types/file.d.ts b/frontend/src/types/file.d.ts index 5664c16b..6b9b6372 100644 --- a/frontend/src/types/file.d.ts +++ b/frontend/src/types/file.d.ts @@ -21,6 +21,7 @@ interface Resource extends ResourceBase { index: number; subtitles?: string[]; content?: string; + rawContent?: ArrayBuffer; } interface ResourceItem extends ResourceBase { @@ -57,3 +58,8 @@ interface BreadCrumb { name: string; url: string; } + +interface CsvData { + headers: string[]; + rows: string[][]; +} diff --git a/frontend/src/utils/csv.ts b/frontend/src/utils/csv.ts deleted file mode 100644 index c03731e7..00000000 --- a/frontend/src/utils/csv.ts +++ /dev/null @@ -1,64 +0,0 @@ -export interface CsvData { - headers: string[]; - rows: string[][]; -} - -/** - * Parse CSV content into headers and rows - * Supports quoted fields and handles commas within quotes - */ -export function parseCSV( - content: string, - columnSeparator: Array -): CsvData { - if (!content || content.trim().length === 0) { - return { headers: [], rows: [] }; - } - - const lines = content.split(/\r?\n/); - const result: string[][] = []; - - for (const line of lines) { - if (line.trim().length === 0) continue; - - const row: string[] = []; - let currentField = ""; - let inQuotes = false; - - for (let i = 0; i < line.length; i++) { - const char = line[i]; - const nextChar = line[i + 1]; - - if (char === '"') { - if (inQuotes && nextChar === '"') { - // Escaped quote - currentField += '"'; - i++; // Skip next quote - } else { - // Toggle quote state - inQuotes = !inQuotes; - } - } else if (columnSeparator.includes(char) && !inQuotes) { - // Field separator - row.push(currentField); - currentField = ""; - } else { - currentField += char; - } - } - - // Add the last field - row.push(currentField); - result.push(row); - } - - if (result.length === 0) { - return { headers: [], rows: [] }; - } - - // First row is headers - const headers = result[0]; - const rows = result.slice(1); - - return { headers, rows }; -} diff --git a/frontend/src/utils/encodings.ts b/frontend/src/utils/encodings.ts new file mode 100644 index 00000000..80f1e219 --- /dev/null +++ b/frontend/src/utils/encodings.ts @@ -0,0 +1,95 @@ +export const availableEncodings = [ + "utf-8", + "ibm866", + "iso-8859-2", + "iso-8859-3", + "iso-8859-4", + "iso-8859-5", + "iso-8859-6", + "iso-8859-7", + "iso-8859-8", + "iso-8859-8-i", + "iso-8859-10", + "iso-8859-13", + "iso-8859-14", + "iso-8859-15", + "iso-8859-16", + "koi8-r", + "koi8-u", + "macintosh", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + "x-mac-cyrillic", + "gbk", + "gb18030", + "big5", + "euc-jp", + "iso-2022-jp", + "shift_jis", + "euc-kr", + "utf-16be", + "utf-16le", +]; + +export function decode(content: ArrayBuffer, encoding: string): string { + const decoder = new TextDecoder(encoding); + return decoder.decode(content); +} + +export function isEncodableResponse(url: string): boolean { + const extensions = [".csv"]; + + if (typeof TextDecoder === "undefined") { + return false; + } + + for (const extension of extensions) { + if (url.endsWith(extension)) { + return true; + } + } + + return false; +} + +export async function makeRawResource( + res: Response, + url: string +): Promise { + const buffer = await res.arrayBuffer(); + return { + items: [], + numDirs: 0, + numFiles: 0, + sorting: {} as Sorting, + index: 0, + extension: getExtension(url), + isDir: false, + isSymlink: false, + path: url, + size: buffer.byteLength, + modified: new Date().toISOString(), + name: url.split("/").pop() || "", + type: "text", + mode: 0, + url: `/files${url}`, + rawContent: buffer, + content: decode(buffer, "utf-8"), + }; +} + +function getExtension(url: string): string { + const lastDotIndex = url.lastIndexOf("."); + if (lastDotIndex === -1) { + return ""; + } + return url.substring(lastDotIndex); +} diff --git a/frontend/src/views/files/Editor.vue b/frontend/src/views/files/Editor.vue index 7d3a8dfc..4ce7b427 100644 --- a/frontend/src/views/files/Editor.vue +++ b/frontend/src/views/files/Editor.vue @@ -171,25 +171,19 @@ onMounted(() => { `https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/` ); - editor.value = ace.edit("editor", { - value: fileContent, - showPrintMargin: false, - readOnly: fileStore.req?.type === "textImmutable", - theme: getEditorTheme(authStore.user?.aceEditorTheme ?? ""), - mode: modelist.getModeForPath(fileStore.req!.name).mode, - wrap: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - enableSnippets: true, - }); - - editor.value.setFontSize(fontSize.value); - editor.value.focus(); - - editor.value.getSelection().on("changeSelection", () => { - isSelectionEmpty.value = - editor.value == null || editor.value.getSelectedText().length == 0; - }); + if (!layoutStore.loading) { + initEditor(fileContent); + } else { + const unwatch = watchEffect(() => { + // Initialize editor when layout is loaded + if (!layoutStore.loading) { + setTimeout(() => { + initEditor(fileContent); + unwatch(); + }, 50); + } + }); + } }); onBeforeUnmount(() => { @@ -218,6 +212,23 @@ onBeforeRouteUpdate((to, from, next) => { }); }); +const initEditor = (fileContent: string) => { + editor.value = ace.edit("editor", { + value: fileContent, + showPrintMargin: false, + readOnly: fileStore.req?.type === "textImmutable", + theme: getEditorTheme(authStore.user?.aceEditorTheme ?? ""), + mode: modelist.getModeForPath(fileStore.req!.name).mode, + wrap: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + enableSnippets: true, + }); + + editor.value.setFontSize(fontSize.value); + editor.value.focus(); +}; + const keyEvent = (event: KeyboardEvent) => { if (event.code === "Escape") { close(); diff --git a/frontend/src/views/files/Preview.vue b/frontend/src/views/files/Preview.vue index 09562856..46670a35 100644 --- a/frontend/src/views/files/Preview.vue +++ b/frontend/src/views/files/Preview.vue @@ -253,7 +253,7 @@ const hoverNav = ref(false); const autoPlay = ref(false); const previousRaw = ref(""); const nextRaw = ref(""); -const csvContent = ref(""); +const csvContent = ref(""); const csvError = ref(""); const player = ref(null); @@ -393,7 +393,11 @@ const updatePreview = async () => { if (fileStore.req.size > CSV_MAX_SIZE) { csvError.value = t("files.csvTooLarge"); } else { - csvContent.value = fileStore.req.content ?? ""; + if (fileStore.req.rawContent != null) { + csvContent.value = fileStore.req.rawContent; + } else { + csvContent.value = fileStore.req.content ?? ""; + } } } diff --git a/http/resource.go b/http/resource.go index 59ce7f5b..7066f35a 100644 --- a/http/resource.go +++ b/http/resource.go @@ -36,10 +36,31 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d return errToStatus(err), err } + encoding := r.Header.Get("X-Encoding") if file.IsDir { file.Sorting = d.user.Sorting file.ApplySort() return renderJSON(w, r, file) + } else if encoding == "true" { + if file.Type != "text" { + return http.StatusUnsupportedMediaType, fmt.Errorf("file is not a text file") + } + + f, err := d.user.Fs.Open(r.URL.Path) + if err != nil { + return errToStatus(err), err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + _, err = w.Write(data) + return 0, err } if checksum := r.URL.Query().Get("checksum"); checksum != "" {