feat: music scanner, settings page & api

This commit is contained in:
2025-06-07 00:04:39 +03:00
parent b068a9576a
commit 1ce788bb33
17 changed files with 923 additions and 348 deletions

171
CHANGELOG.md Normal file
View File

@@ -0,0 +1,171 @@
# Changelog
## [0.7.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.6.4...v0.7.0) (2025-05-28)
### Features
- add localizations
([5910320](https://github.com/chenasraf/nextcloud-jukebox/commit/5910320b90507cc65a89d2bffb2d24f39d2a15ca))
### Bug Fixes
- **ApiController:** use argument param in updateSettings
([aba72a1](https://github.com/chenasraf/nextcloud-jukebox/commit/aba72a13f2a6379ee128ee5ffb21a3fe1ea8ccdc))
- rounding
([ff02782](https://github.com/chenasraf/nextcloud-jukebox/commit/ff027827de1ac3d70cf7eeb818dbebdaf5b2e4a2))
## [0.6.4](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.6.3...v0.6.4) (2025-01-18)
### Bug Fixes
- skip projects with missing default currency
([d015f26](https://github.com/chenasraf/nextcloud-jukebox/commit/d015f26bc2a953367cdcb4c365f2633b486f1b4d))
## [0.6.3](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.6.2...v0.6.3) (2024-12-25)
### Bug Fixes
- support cospend v3.0.7
([39f1a9e](https://github.com/chenasraf/nextcloud-jukebox/commit/39f1a9efc0af68ae6a2f3cf5b3c769957da75405)),
closes [#33](https://github.com/chenasraf/nextcloud-jukebox/issues/33)
## [0.6.2](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.6.1...v0.6.2) (2024-12-21)
### Bug Fixes
- log level + methods docstring for currency
([dff6d94](https://github.com/chenasraf/nextcloud-jukebox/commit/dff6d947d3fe857d95ae028eb9383a7600ad27f4))
## [0.6.1](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.6.0...v0.6.1) (2024-12-08)
### Bug Fixes
- appstore script path resolution
([1c02b79](https://github.com/chenasraf/nextcloud-jukebox/commit/1c02b796c55074a6afd1fec3c5aaf815f0947f75))
## [0.6.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.5.2...v0.6.0) (2024-12-08)
### Features
- add supported currencies table
([b05290b](https://github.com/chenasraf/nextcloud-jukebox/commit/b05290beab361e44354df489074d40b93c4ea2e5))
### Bug Fixes
- sort currencies table
([56e160f](https://github.com/chenasraf/nextcloud-jukebox/commit/56e160f3a19bd6e0a399c0236c14899edc25a4b2))
## [0.5.2](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.5.1...v0.5.2) (2024-12-07)
### Bug Fixes
- add missing eur currency info
([7948fd9](https://github.com/chenasraf/nextcloud-jukebox/commit/7948fd9a456654c0d81ab73501d0a87056ea49cb))
## [0.5.1](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.5.0...v0.5.1) (2024-12-06)
### Bug Fixes
- settings section priority
([da05142](https://github.com/chenasraf/nextcloud-jukebox/commit/da0514250882472b7b3ef0f9f293e0cf6f5568a5))
## [0.5.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.4.0...v0.5.0) (2024-12-05)
### Features
- update admin page
([ab029a3](https://github.com/chenasraf/nextcloud-jukebox/commit/ab029a3ecdec763dbe79ef38d8e0bf1676ef00b4))
- update admin page ui
([32c2c94](https://github.com/chenasraf/nextcloud-jukebox/commit/32c2c94526148efe767584c79ef8a380f26c0252))
- update app+settings icons
([b85256f](https://github.com/chenasraf/nextcloud-jukebox/commit/b85256f5a236b5701013878b18e03e1c8baabd07))
## [0.4.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.3.0...v0.4.0) (2024-12-05)
### Features
- add admin page intro section
([023b2fd](https://github.com/chenasraf/nextcloud-jukebox/commit/023b2fd61c28cfdcb9a787b4cb4b5d853dffcdad))
### Bug Fixes
- only include available currencies
([902522f](https://github.com/chenasraf/nextcloud-jukebox/commit/902522f20f29382a837c0062a0c08c3f681cef73))
## [0.3.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.2.1...v0.3.0) (2024-12-04)
### Features
- improve currency matching
([8738623](https://github.com/chenasraf/nextcloud-jukebox/commit/87386235c22a6dcd09f17cbeaa094152ccfd8540))
## [0.2.1](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.2.0...v0.2.1) (2024-12-03)
### Bug Fixes
- release artifact tar
([8d632ef](https://github.com/chenasraf/nextcloud-jukebox/commit/8d632ef7f215255246f209ab6e0593ef786e2bfc))
## [0.2.0](https://github.com/chenasraf/nextcloud-jukebox/compare/v0.1.0...v0.2.0) (2024-12-03)
### Features
- add admin example
([1b3804b](https://github.com/chenasraf/nextcloud-jukebox/commit/1b3804ba0d8f73687c4260fbb2f20aac4470b758))
- admin page view
([68ae6eb](https://github.com/chenasraf/nextcloud-jukebox/commit/68ae6eb09e35057e426072c9986c7965d29401ea))
- api controller poc
([802c72f](https://github.com/chenasraf/nextcloud-jukebox/commit/802c72f0f7dd9be5f9abc3829ff403b9abfda7f8))
- settings page logic
([20d66b3](https://github.com/chenasraf/nextcloud-jukebox/commit/20d66b3650f53701a9a9ec54ac9cf15961592ced))
- update name
([ab7c03b](https://github.com/chenasraf/nextcloud-jukebox/commit/ab7c03b42475f701a151f383f06170f999d51c75))
### Bug Fixes
- app info
([aebc8a5](https://github.com/chenasraf/nextcloud-jukebox/commit/aebc8a52cc49cb736ce5e78f23ddc0626006a4d1))
- build cmd
([4046b7b](https://github.com/chenasraf/nextcloud-jukebox/commit/4046b7b8df01bce39fa4f31947971166b8f4aa56))
- cron
([5aebc7a](https://github.com/chenasraf/nextcloud-jukebox/commit/5aebc7a2aa46fba0daf403f11baa731620e335ae))
- tests + use IAppConfig
([b6058ef](https://github.com/chenasraf/nextcloud-jukebox/commit/b6058eff576790620f8b8166550d903872731f1d))
- typescript version
([c4f625a](https://github.com/chenasraf/nextcloud-jukebox/commit/c4f625a19236df7834a68b6a7d75c8b27d5113e6))
- updateSettings ep + types
([aae1dbc](https://github.com/chenasraf/nextcloud-jukebox/commit/aae1dbc141ab9c6ee8d57682e283d8615e3c4c91))
## 0.1.0 (2024-12-03)
### Features
- add admin example
([1b3804b](https://github.com/chenasraf/nextcloud-jukebox/commit/1b3804ba0d8f73687c4260fbb2f20aac4470b758))
- admin page view
([68ae6eb](https://github.com/chenasraf/nextcloud-jukebox/commit/68ae6eb09e35057e426072c9986c7965d29401ea))
- api controller poc
([802c72f](https://github.com/chenasraf/nextcloud-jukebox/commit/802c72f0f7dd9be5f9abc3829ff403b9abfda7f8))
- poc ready
([e54cb41](https://github.com/chenasraf/nextcloud-jukebox/commit/e54cb41c5b549294fc8b014ef2a507178f4e8597))
- poc working
([c32cdaf](https://github.com/chenasraf/nextcloud-jukebox/commit/c32cdaf38de64f45de1285463f4265da2e95b438))
- settings page logic
([20d66b3](https://github.com/chenasraf/nextcloud-jukebox/commit/20d66b3650f53701a9a9ec54ac9cf15961592ced))
- update name
([ab7c03b](https://github.com/chenasraf/nextcloud-jukebox/commit/ab7c03b42475f701a151f383f06170f999d51c75))
### Bug Fixes
- app info
([aebc8a5](https://github.com/chenasraf/nextcloud-jukebox/commit/aebc8a52cc49cb736ce5e78f23ddc0626006a4d1))
- build cmd
([4046b7b](https://github.com/chenasraf/nextcloud-jukebox/commit/4046b7b8df01bce39fa4f31947971166b8f4aa56))
- cron
([5aebc7a](https://github.com/chenasraf/nextcloud-jukebox/commit/5aebc7a2aa46fba0daf403f11baa731620e335ae))
- tests + use IAppConfig
([b6058ef](https://github.com/chenasraf/nextcloud-jukebox/commit/b6058eff576790620f8b8166550d903872731f1d))
- typescript version
([c4f625a](https://github.com/chenasraf/nextcloud-jukebox/commit/c4f625a19236df7834a68b6a7d75c8b27d5113e6))
- updateSettings ep + types
([aae1dbc](https://github.com/chenasraf/nextcloud-jukebox/commit/aae1dbc141ab9c6ee8d57682e283d8615e3c4c91))

View File

@@ -47,7 +47,10 @@ It supports music files, podcasts (with gPodder sync), audiobooks, YouTube video
<job>OCA\Jukebox\Cron\FetchCurrenciesJob</job>
</background-jobs> -->
<settings>
<admin>OCA\Jukebox\Settings\CurrencyAdmin</admin>
<admin-section>OCA\Jukebox\Sections\CurrencyAdmin</admin-section>
<personal>OCA\Jukebox\Settings\JukeboxUserSettings</personal>
<personal-section>OCA\Jukebox\Sections\JukeboxUserSection</personal-section>
</settings>
<commands>
<command>OCA\Jukebox\Command\ScanMusic</command>
</commands>
</info>

View File

@@ -21,6 +21,7 @@ class Application extends App implements IBootstrap {
}
public function register(IRegistrationContext $context): void {
include_once __DIR__ . '/../../vendor/autoload.php';
}
public function boot(IBootContext $context): void {

51
lib/Command/ScanMusic.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Chen Asraf <casraf@pm.me>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Jukebox\Command;
use OCA\Jukebox\Service\MusicScanner;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ScanMusic extends Command {
public function __construct(
private MusicScanner $service,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('jukebox:scan-music')
->setDescription('Scan music files for a user')
->addArgument('uid', InputArgument::OPTIONAL, 'User ID to scan. If not provided, uses the current session user.');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$uid = $input->getArgument('uid');
try {
if ($uid) {
$output->writeln("<info>Scanning music files for user '$uid'...</info>");
$this->service->scanUserByUID($uid);
} else {
$output->writeln('<info>Scanning music files for the current session user...</info>');
$this->service->scanMusicFiles();
}
$output->writeln('<info>Scan completed successfully.</info>');
return Command::SUCCESS;
} catch (\Throwable $e) {
$output->writeln('<error>Scan failed: ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace OCA\Jukebox\Controller;
use OCA\Jukebox\AppInfo\Application;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IAppConfig;
use OCP\IRequest;
use OCP\IUserSession;
/**
* Handles user-specific settings such as the music folder path.
*/
class SettingsController extends OCSController {
private IAppConfig $config;
private IUserSession $userSession;
public function __construct(
string $appName,
IRequest $request,
IAppConfig $config,
IUserSession $userSession,
) {
parent::__construct($appName, $request);
$this->config = $config;
$this->userSession = $userSession;
}
/**
* Save user-specific settings
*
* @param array<string, mixed> $data
* @return DataResponse<Http::STATUS_OK, array{status: non-empty-string}, array{}>
*
* 200: Settings saved
*/
#[ApiRoute(verb: 'PUT', url: '/api/settings')]
public function saveSettings(mixed $data): DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse(['status' => 'unauthenticated'], Http::STATUS_UNAUTHORIZED);
}
$uid = $user->getUID();
if (array_key_exists('music_folder_path', $data)) {
$this->config->setValueString(Application::APP_ID, 'music_folder_path_' . $uid, $data['music_folder_path']);
}
return new DataResponse(['status' => 'OK']);
}
/**
* Fetch all user-specific settings
*
* @return DataResponse<Http::STATUS_OK, array<string, string>, array{}>
*
* 200: Current settings
*/
#[ApiRoute(verb: 'GET', url: '/api/settings')]
public function getSettings(): DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
$uid = $user->getUID();
$result = [];
$musicPath = $this->config->getValueString(Application::APP_ID, 'music_folder_path_' . $uid, 'Music');
if ($musicPath !== null) {
$result['music_folder_path'] = $musicPath;
}
return new DataResponse($result);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace OCA\Jukebox\Sections;
use OCA\Jukebox\AppInfo\Application;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class JukeboxUserSection implements IIconSection {
private IL10N $l;
private IURLGenerator $urlGenerator;
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
$this->l = $l;
$this->urlGenerator = $urlGenerator;
}
public function getIcon(): string {
return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg');
}
public function getID(): string {
return Application::APP_ID;
}
public function getName(): string {
return $this->l->t('Jukebox');
}
public function getPriority(): int {
return 50;
}
}

View File

@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace OCA\Jukebox\Service;
use getID3;
use OCA\Jukebox\AppInfo\Application;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IAppConfig;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
@@ -22,37 +23,55 @@ class MusicScanner {
private IRootFolder $rootFolder;
private IUserSession $userSession;
/**
* @param IRootFolder $rootFolder
* @param IUserSession $userSession
* @param LoggerInterface $logger
*/
public function __construct(
IRootFolder $rootFolder,
IUserSession $userSession,
private LoggerInterface $logger,
private IAppConfig $appConfig,
) {
$this->rootFolder = $rootFolder;
$this->userSession = $userSession;
}
/**
* Starts scanning the authenticated user's folder for music files.
* Starts scanning the user's configured music directory for audio files.
*
* @return void
*/
public function scanMusicFiles(): void {
$user = $this->userSession->getUser();
if ($user === null) {
// Handle unauthenticated user
$this->logger->warning('Music scan aborted: no user session.');
return;
}
$this->scanUserByUID($user->getUID());
}
/**
* Scans the music directory for a specific user by UID.
*
* @param string $uid
* @return void
*/
public function scanUserByUID(string $uid): void {
try {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$this->traverseFolder($userFolder, $user);
$userFolder = $this->rootFolder->getUserFolder($uid);
$relativePath = $this->appConfig->getValueString(Application::APP_ID, 'music_folder_path_' . $uid, 'Music');
/** @var Folder $musicFolder */
$musicFolder = $userFolder->get($relativePath);
if (!($musicFolder instanceof Folder)) {
$this->logger->warning("Configured music path '$relativePath' for user $uid is not a folder.");
return;
}
$this->logger->info("Starting music scan for user '$uid' in folder '$relativePath'");
$this->traverseFolder($musicFolder, $uid);
} catch (NotFoundException $e) {
// Handle folder not found
$this->logger->error("Could not find music folder for user $uid: " . $e->getMessage());
}
}
@@ -60,18 +79,21 @@ class MusicScanner {
* Recursively traverses a folder and processes audio files.
*
* @param Folder $folder
* @param IUser $user
* @param string $uid
* @return void
*/
private function traverseFolder(Folder $folder, IUser $user): void {
private function traverseFolder(Folder $folder, string $uid): void {
$this->logger->info('Scanning folder: ' . $folder->getPath());
foreach ($folder->getDirectoryListing() as $node) {
if ($node instanceof File) {
$mimeType = $node->getMimeType();
if (str_starts_with($mimeType, 'audio/')) {
$this->processAudioFile($node, $user);
$this->logger->info('Found audio file: ' . $node->getPath() . " (MIME: $mimeType)");
$this->processAudioFile($node, $uid);
}
} elseif ($node instanceof Folder) {
$this->traverseFolder($node, $user);
$this->traverseFolder($node, $uid);
}
}
}
@@ -80,24 +102,28 @@ class MusicScanner {
* Downloads an audio file, reads metadata, and processes it.
*
* @param File $file
* @param IUser $user
* @param string $uid
* @return void
*/
private function processAudioFile(File $file, IUser $user): void {
private function processAudioFile(File $file, string $uid): void {
$this->logger->info('Processing audio file: ' . $file->getPath());
$tempPath = tempnam(sys_get_temp_dir(), 'jukebox_');
if ($tempPath === false) {
$this->logger->error('Could not create temporary file for audio processing.');
return; // Could not create temp file
return;
}
$handle = fopen($tempPath, 'w');
if ($handle === false) {
$this->logger->error('Failed to open temporary file handle.');
return;
}
$stream = $file->fopen('r');
if ($stream === false) {
fclose($handle);
$this->logger->error('Failed to open file stream for ' . $file->getPath());
return;
}
@@ -108,17 +134,24 @@ class MusicScanner {
$getID3 = new getID3();
$info = $getID3->analyze($tempPath);
$artist = $info['tags']['id3v2']['artist'][0] ?? '';
$songArtist = $info['tags']['id3v2']['artist'][0] ?? '';
$albumArtist =
$info['tags']['id3v2']['band'][0] ??
$info['tags']['id3v2']['album_artist'][0] ??
$info['tags']['quicktime']['album_artist'][0] ??
$info['tags']['asf']['WM/AlbumArtist'][0] ??
'';
$album = $info['tags']['id3v2']['album'][0] ?? '';
$title = $info['tags']['id3v2']['title'][0] ?? $file->getName();
// Clean up temp file
$this->logger->info("Scanned metadata for '{$file->getPath()}': Song Artist='{$songArtist}', Album Artist='{$albumArtist}', Album='{$album}', Title='{$title}'");
unlink($tempPath);
// Stub: Save metadata to DB
$this->saveMetadataToDatabase($user->getUID(), $file->getId(), $artist, $album, $title);
$this->saveMetadataToDatabase($uid, $file->getId(), $songArtist, $album, $title);
}
/**
* Placeholder method to save music metadata.
*

View File

@@ -0,0 +1,44 @@
<?php
namespace OCA\Jukebox\Settings;
use OCA\Jukebox\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IAppConfig;
use OCP\IL10N;
use OCP\Settings\ISettings;
use OCP\Util;
class JukeboxAdmin implements ISettings {
private IL10N $l;
private IAppConfig $config;
public function __construct(IAppConfig $config, IL10N $l) {
$this->config = $config;
$this->l = $l;
}
/**
* @return TemplateResponse
*/
public function getForm(): TemplateResponse {
Util::addScript(Application::APP_ID, Application::JS_DIR . '/Jukebox-main');
Util::addStyle(Application::APP_ID, Application::CSS_DIR . '/Jukebox-style');
return new TemplateResponse(Application::APP_ID, 'settings', [], '');
}
public function getSection(): string {
return Application::APP_ID;
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority(): int {
return 10;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace OCA\Jukebox\Settings;
use OCA\Jukebox\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IAppConfig;
use OCP\IL10N;
use OCP\Settings\ISettings;
use OCP\Util;
/**
* Settings form shown under user's personal settings.
*/
class JukeboxUserSettings implements ISettings {
private IL10N $l;
private IAppConfig $config;
public function __construct(IAppConfig $config, IL10N $l) {
$this->config = $config;
$this->l = $l;
}
public function getForm(): TemplateResponse {
Util::addScript(Application::APP_ID, Application::JS_DIR . '/Jukebox-settings');
Util::addStyle(Application::APP_ID, Application::CSS_DIR . '/Jukebox-style');
return new TemplateResponse(Application::APP_ID, 'settings', []);
}
public function getSection(): string {
return Application::APP_ID;
}
public function getPriority(): int {
return 10;
}
}

View File

@@ -20,6 +20,7 @@
],
"dependencies": {
"@nextcloud/axios": "^2.5.1",
"@nextcloud/dialogs": "7.0.0-rc.0",
"@nextcloud/l10n": "^3.3.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vite-config": "^2.3.5",

249
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@nextcloud/axios':
specifier: ^2.5.1
version: 2.5.1
'@nextcloud/dialogs':
specifier: 7.0.0-rc.0
version: 7.0.0-rc.0(typescript@5.8.3)
'@nextcloud/l10n':
specifier: ^3.3.0
version: 3.3.0
@@ -165,6 +168,9 @@ packages:
'@bufbuild/protobuf@2.5.2':
resolution: {integrity: sha512-foZ7qr0IsUBjzWIq+SuBLfdQCpJ1j8cTuNNT4owngTHoN5KsJb8L9t65fzz7SCeSWzescoOil/0ldqiL041ABg==}
'@buttercup/fetch@0.2.1':
resolution: {integrity: sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==}
'@ckpack/vue-color@1.6.0':
resolution: {integrity: sha512-b9kFTKhYbNArfgP1lmnaVm0VNsWdZjqIbyHUYry7mZ+E7JeTQclbjq1+2xWn0SE3wzqRYlXmAVjECPOgteWmMQ==}
engines: {node: '>=12'}
@@ -443,6 +449,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@mdi/js@7.4.47':
resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
'@microsoft/api-extractor-model@7.30.6':
resolution: {integrity: sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==}
@@ -476,6 +485,10 @@ packages:
resolution: {integrity: sha512-L1NQtOfHWzkfj0Ple1MEJt6HmOHWAi3y4qs+OnwSWexqJT0DtXTVPyRxi7ADyITwRxS5H9R/HMl6USAj4Nr1nQ==}
engines: {node: ^20.0.0, npm: ^10.0.0}
'@nextcloud/dialogs@7.0.0-rc.0':
resolution: {integrity: sha512-RqoXbSBRzxPUU4g4f5c3sRWwpL6tnuSifZSW+L/xpT5GRfLbzF+tGkMgN1ghC9rmiLb0B5ovPWqABZiwrid9Rw==}
engines: {node: ^20 || ^22, npm: ^10.0.0}
'@nextcloud/eslint-config@8.4.2':
resolution: {integrity: sha512-zsDcBxvp2Vr/BgasK/vNYJ84LOXjl4RseJPrcp93zcnaB2WnygV50Sd0nQ5JN0ngTyPjiIlGd92MMzrMTofjRA==}
engines: {node: ^20.0.0, npm: ^10.0.0}
@@ -505,6 +518,10 @@ packages:
resolution: {integrity: sha512-1Qfs6i7Tz2qd1A33NpBQOt810ydHIRjhyXMFwSEkYX2yUI80lAk/sWO8HIB2Fqp+iffhyviPPcQYoytMDRyDNw==}
engines: {node: ^20.0.0, npm: ^10.0.0}
'@nextcloud/files@3.10.2':
resolution: {integrity: sha512-8k6zN3nvGW8nEV5Db5DyfqcyK99RWw1iOSPIafi2RttiRQGpFzHlnF2EoM4buH5vWzI39WEvJnfuLZpkPX0cFw==}
engines: {node: ^20.0.0, npm: ^10.0.0}
'@nextcloud/initial-state@2.2.0':
resolution: {integrity: sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==}
engines: {node: ^20.0.0, npm: ^10.0.0}
@@ -517,6 +534,10 @@ packages:
resolution: {integrity: sha512-wByt0R0/6QC44RBpaJr1MWghjjOxk/pRbACHo/ZWWKht1qYbJRHB4GtEi+35KEIHY07ZpqxiDk6dIRuN7sXYWQ==}
engines: {node: ^20.0.0, npm: ^10.0.0}
'@nextcloud/paths@2.2.1':
resolution: {integrity: sha512-M3ShLjrxR7B48eKThLMoqbxTqTKyQXcwf9TgeXQGbCIhiHoXU6as5j8l5qNv/uZlANokVdowpuWHBi3b2+YNNA==}
engines: {node: ^20.0.0, npm: ^10.0.0}
'@nextcloud/router@3.0.1':
resolution: {integrity: sha512-Ci/uD3x8OKHdxSqXL6gRJ+mGJOEXjeiHjj7hqsZqVTsT7kOrCjDf0/J8z5RyLlokKZ0IpSe+hGxgi3YB7Gpw3Q==}
engines: {node: ^20.0.0, npm: ^10.0.0}
@@ -851,6 +872,9 @@ packages:
'@types/sizzle@2.3.9':
resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==}
'@types/toastify-js@1.12.4':
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1235,6 +1259,9 @@ packages:
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base-64@1.0.0:
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -1317,6 +1344,9 @@ packages:
builtins@5.1.0:
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
byte-length@1.0.2:
resolution: {integrity: sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1333,6 +1363,9 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
cancelable-promise@4.3.1:
resolution: {integrity: sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==}
caniuse-lite@1.0.30001721:
resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==}
@@ -1359,6 +1392,9 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -1471,6 +1507,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
crypto-browserify@3.12.1:
resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
engines: {node: '>= 0.10'}
@@ -1491,6 +1530,10 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -1623,6 +1666,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.0:
resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
engines: {node: '>=0.12'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -1902,6 +1949,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -1958,6 +2009,10 @@ packages:
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
engines: {node: '>= 6'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fs-extra@11.3.0:
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
engines: {node: '>=14.14'}
@@ -2106,6 +2161,9 @@ packages:
hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
hot-patcher@2.0.1:
resolution: {integrity: sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==}
html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@@ -2200,6 +2258,9 @@ packages:
resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
engines: {node: '>= 0.4'}
is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
is-builtin-module@3.2.1:
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
engines: {node: '>=6'}
@@ -2301,6 +2362,10 @@ packages:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
is-svg@5.1.0:
resolution: {integrity: sha512-uVg5yifaTxHoefNf5Jcx+i9RZe2OBYd/UStp1umx+EERa4xGRa3LLGXjoEph43qUORC0qkafUgrXZ6zzK89yGA==}
engines: {node: '>=14.16'}
is-symbol@1.1.1:
resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
engines: {node: '>= 0.4'}
@@ -2399,6 +2464,9 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
layerr@3.0.0:
resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -2472,6 +2540,9 @@ packages:
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
mdast-squeeze-paragraphs@6.0.0:
resolution: {integrity: sha512-6NDbJPTg0M0Ye+TlYwX1KJ1LFbp515P2immRJyJQhc9Na9cetHzSoHNYIQcXpANEAP1sm9yd/CTZU2uHqR5A+w==}
@@ -2642,9 +2713,21 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
nested-property@4.0.0:
resolution: {integrity: sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -2754,6 +2837,9 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-posix@1.0.0:
resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -2884,6 +2970,9 @@ packages:
resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==}
engines: {node: '>=0.4.x'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -2941,6 +3030,9 @@ packages:
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
engines: {node: '>=0.10.5'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3426,6 +3518,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
toastify-js@1.12.0:
resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==}
tributejs@5.1.3:
resolution: {integrity: sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==}
@@ -3487,6 +3582,9 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript-event-target@1.1.1:
resolution: {integrity: sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==}
typescript@5.8.2:
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
engines: {node: '>=14.17'}
@@ -3544,6 +3642,13 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-join@5.0.0:
resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
url@0.11.4:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
@@ -3663,6 +3768,14 @@ packages:
typescript:
optional: true
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webdav@5.8.0:
resolution: {integrity: sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==}
engines: {node: '>=14'}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -3843,6 +3956,10 @@ snapshots:
'@bufbuild/protobuf@2.5.2': {}
'@buttercup/fetch@0.2.1':
optionalDependencies:
node-fetch: 3.3.2
'@ckpack/vue-color@1.6.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@ctrl/tinycolor': 3.6.1
@@ -4038,6 +4155,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@mdi/js@7.4.47': {}
'@microsoft/api-extractor-model@7.30.6(@types/node@20.17.10)':
dependencies:
'@microsoft/tsdoc': 0.15.1
@@ -4096,6 +4215,33 @@ snapshots:
dependencies:
'@nextcloud/initial-state': 2.2.0
'@nextcloud/dialogs@7.0.0-rc.0(typescript@5.8.3)':
dependencies:
'@mdi/js': 7.4.47
'@nextcloud/auth': 2.5.1
'@nextcloud/axios': 2.5.1
'@nextcloud/event-bus': 3.3.2
'@nextcloud/files': 3.10.2
'@nextcloud/initial-state': 2.2.0
'@nextcloud/l10n': 3.3.0
'@nextcloud/paths': 2.2.1
'@nextcloud/router': 3.0.1
'@nextcloud/sharing': 0.2.4
'@nextcloud/typings': 1.9.1
'@nextcloud/vue': 9.0.0-rc.2(typescript@5.8.3)
'@types/toastify-js': 1.12.4
'@vueuse/core': 13.3.0(vue@3.5.16(typescript@5.8.3))
cancelable-promise: 4.3.1
p-queue: 8.1.0
toastify-js: 1.12.0
vue: 3.5.16(typescript@5.8.3)
webdav: 5.8.0
transitivePeerDependencies:
- '@nuxt/kit'
- debug
- supports-color
- typescript
'@nextcloud/eslint-config@8.4.2(@babel/core@7.26.0)(@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@9.28.0))(@nextcloud/eslint-plugin@2.2.1(eslint@9.28.0))(@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.32.0(eslint@9.28.0))(eslint@9.28.0)(typescript@5.8.3))(eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@16.6.2(eslint@9.28.0))(eslint-plugin-promise@6.6.0(eslint@9.28.0))(eslint@9.28.0))(eslint-import-resolver-exports@1.0.0-beta.5(eslint-plugin-import@2.31.0)(eslint@9.28.0))(eslint-import-resolver-typescript@3.6.3)(eslint-plugin-import@2.31.0)(eslint-plugin-jsdoc@46.10.1(eslint@9.28.0))(eslint-plugin-n@16.6.2(eslint@9.28.0))(eslint-plugin-promise@6.6.0(eslint@9.28.0))(eslint-plugin-vue@9.32.0(eslint@9.28.0))(eslint@9.28.0)(typescript@5.8.3)':
dependencies:
'@babel/core': 7.26.0
@@ -4125,6 +4271,20 @@ snapshots:
'@types/semver': 7.7.0
semver: 7.7.2
'@nextcloud/files@3.10.2':
dependencies:
'@nextcloud/auth': 2.5.1
'@nextcloud/capabilities': 1.2.0
'@nextcloud/l10n': 3.3.0
'@nextcloud/logger': 3.0.2
'@nextcloud/paths': 2.2.1
'@nextcloud/router': 3.0.1
'@nextcloud/sharing': 0.2.4
cancelable-promise: 4.3.1
is-svg: 5.1.0
typescript-event-target: 1.1.1
webdav: 5.8.0
'@nextcloud/initial-state@2.2.0': {}
'@nextcloud/l10n@3.3.0':
@@ -4139,6 +4299,8 @@ snapshots:
dependencies:
'@nextcloud/auth': 2.5.1
'@nextcloud/paths@2.2.1': {}
'@nextcloud/router@3.0.1':
dependencies:
'@nextcloud/typings': 1.9.1
@@ -4479,6 +4641,8 @@ snapshots:
'@types/sizzle@2.3.9': {}
'@types/toastify-js@1.12.4': {}
'@types/trusted-types@2.0.7':
optional: true
@@ -4968,6 +5132,8 @@ snapshots:
balanced-match@2.0.0: {}
base-64@1.0.0: {}
base64-js@1.5.1: {}
blurhash@2.0.5: {}
@@ -5071,6 +5237,8 @@ snapshots:
dependencies:
semver: 7.7.2
byte-length@1.0.2: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -5090,6 +5258,8 @@ snapshots:
callsites@3.1.0: {}
cancelable-promise@4.3.1: {}
caniuse-lite@1.0.30001721: {}
ccount@2.0.1: {}
@@ -5109,6 +5279,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
charenc@0.0.2: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -5216,6 +5388,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypt@0.0.2: {}
crypto-browserify@3.12.1:
dependencies:
browserify-cipher: 1.0.1
@@ -5242,6 +5416,8 @@ snapshots:
csstype@3.1.3: {}
data-uri-to-buffer@4.0.1: {}
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -5381,6 +5557,8 @@ snapshots:
entities@4.5.0: {}
entities@6.0.0: {}
env-paths@2.2.1: {}
environment@1.1.0: {}
@@ -5786,6 +5964,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -5839,6 +6022,10 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fs-extra@11.3.0:
dependencies:
graceful-fs: 4.2.11
@@ -6018,6 +6205,8 @@ snapshots:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
hot-patcher@2.0.1: {}
html-tags@3.3.1: {}
htmlparser2@8.0.2:
@@ -6103,6 +6292,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-buffer@1.1.6: {}
is-builtin-module@3.2.1:
dependencies:
builtin-modules: 3.3.0
@@ -6195,6 +6386,10 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-svg@5.1.0:
dependencies:
fast-xml-parser: 4.5.3
is-symbol@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -6272,6 +6467,8 @@ snapshots:
kolorist@1.8.0: {}
layerr@3.0.0: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -6363,6 +6560,12 @@ snapshots:
inherits: 2.0.4
safe-buffer: 5.2.1
md5@2.3.0:
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: 1.1.6
mdast-squeeze-paragraphs@6.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -6663,9 +6866,19 @@ snapshots:
natural-compare@1.4.0: {}
nested-property@4.0.0: {}
node-addon-api@7.1.1:
optional: true
node-domexception@1.0.0: {}
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-releases@2.0.19: {}
node-stdlib-browser@1.3.1:
@@ -6820,6 +7033,8 @@ snapshots:
path-parse@1.0.7: {}
path-posix@1.0.0: {}
path-type@4.0.0: {}
pathe@2.0.3: {}
@@ -6936,6 +7151,8 @@ snapshots:
querystring-es3@0.2.1: {}
querystringify@2.2.0: {}
queue-microtask@1.2.3: {}
randombytes@2.1.0:
@@ -7043,6 +7260,8 @@ snapshots:
requireindex@1.2.0: {}
requires-port@1.0.0: {}
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -7605,6 +7824,8 @@ snapshots:
dependencies:
is-number: 7.0.0
toastify-js@1.12.0: {}
tributejs@5.1.3: {}
trim-lines@3.0.1: {}
@@ -7679,6 +7900,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
typescript-event-target@1.1.1: {}
typescript@5.8.2: {}
typescript@5.8.3: {}
@@ -7749,6 +7972,13 @@ snapshots:
dependencies:
punycode: 2.3.1
url-join@5.0.0: {}
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
url@0.11.4:
dependencies:
punycode: 1.4.1
@@ -7868,6 +8098,25 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
web-streams-polyfill@3.3.3: {}
webdav@5.8.0:
dependencies:
'@buttercup/fetch': 0.2.1
base-64: 1.0.0
byte-length: 1.0.2
entities: 6.0.0
fast-xml-parser: 4.5.3
hot-patcher: 2.0.1
layerr: 3.0.0
md5: 2.3.0
minimatch: 9.0.5
nested-property: 4.0.0
node-fetch: 3.3.2
path-posix: 1.0.0
url-join: 5.0.0
url-parse: 1.5.10
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0

79
scaffold.config.cjs Normal file
View File

@@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-require-imports */
// eslint-disable-next-line no-undef
const { format } = require('date-fns')
// eslint-disable-next-line no-undef
const fs = require('node:fs')
function getLatestMigration() {
const migrationDir = 'lib/migration'
const files = fs.readdirSync(migrationDir)
const migrationFiles = files.sort((a, b) => a.localeCompare(b))
const latestMigration = migrationFiles[migrationFiles.length - 1]
const matches = /Version(\d+)/.exec(latestMigration)
const version = matches ? Number(matches[1]) + 1 : 0
return version
}
// eslint-disable-next-line no-undef
module.exports = () => {
return {
component: {
templates: ['gen/component'],
output: 'src/components',
subDir: false,
},
page: {
templates: ['gen/page'],
output: 'src/pages',
subDir: false,
},
command: {
templates: ['gen/command'],
output: 'lib/Command',
subDir: false,
},
model: {
templates: ['gen/model'],
output: 'lib/Db',
subDir: false,
},
'task-queued': {
templates: ['gen/task-queued'],
output: 'lib/Cron',
subDir: false,
},
'task-timed': {
templates: ['gen/task-timed'],
output: 'lib/Cron',
subDir: false,
},
service: {
templates: ['gen/service'],
output: 'lib/Service',
subDir: false,
},
util: {
templates: ['gen/util'],
output: 'lib/Util',
subDir: false,
},
api: {
templates: ['gen/api'],
output: 'lib/Controller',
subDir: false,
},
migration: () => {
const latestMigrationVersion = getLatestMigration()
return {
templates: ['gen/migration'],
output: 'lib/Migration',
name: '-',
data: {
version: latestMigrationVersion,
dt: format(new Date(), 'yyyyMMddHHmmss'),
},
}
},
}
}

View File

@@ -1,220 +1,71 @@
<template>
<div id="jukebox-content" class="section">
<div id="jukebox-user-settings" class="section">
<h2>Jukebox</h2>
<NcAppSettingsSection name="Information"
>
<p> {{ strings.info }} </p>
<h2>{{ strings.header }}</h2>
<p v-html="strings.requirements"></p>
<ol class="ol">
<li v-for="li in strings.requirementsList" v-html="li"></li>
</ol>
<NcNoteCard type="info"
<form @submit.prevent="save">
<NcAppSettingsSection :name="strings.musicLibrarySettings"
>
<p v-html="strings.infoNote"></p>
</NcNoteCard
>
<p>{{ strings.exampleHeader }}</p>
<div class="folder-select-wrapper">
<ul>
<li> <code>$</code></li>
<li> <code>USD</code></li>
<li> <code>$ USD</code></li>
<li> <code>US Dollar</code></li>
<li> <code>United States Dollar</code></li>
</ul>
<div class="currency-list">
<p>{{ strings.supportedCurrencies }}</p>
<div style="max-width: 300px">
<NcTextField
v-model="currencySearch"
:label="strings.currencySearchLabel"
trailing-button-icon="close"
:placeholder="strings.currencySearchPlaceholder"
:show-trailing-button="currencySearch !== ''"
@trailing-button-click="clearCurrencySearch"
/>
</div>
<table>
<thead>
<tr>
<th>{{ strings.tableSymbol }}</th>
<th>{{ strings.tableCode }}</th>
<th>{{ strings.tableName }}</th>
</tr>
</thead>
<tbody>
<tr v-for="currency in currencies" :key="currency.code">
<td>{{ currency.symbol }}</td>
<td>{{ currency.code }}</td>
<td>{{ currency.name }}</td>
</tr>
</tbody>
</table>
</div>
</NcAppSettingsSection
> <NcAppSettingsSection :name="strings.cronSettingsHeader"
>
<section>
<form @submit.prevent @submit="save">
<div class="cron-flex">
<NcSelect
v-model="interval"
:options="intervals"
:input-label="strings.intervalLabel"
required
<div class="input-with-button">
<NcTextField
v-model="musicFolder"
:label="strings.musicFolderLabel"
:placeholder="strings.musicFolderPlaceholder"
:disabled="true"
/> <NcButton
@click="openFolderPicker"
icon="icon-folder"
:aria-label="strings.pickFolder"
:title="strings.pickFolder"
:disabled="loading"
/>
<div class="cron-last-update-container">
<NcButton @click="doCron" :disabled="loading">{{ strings.fetchNow }}</NcButton
>
<div>
{{ strings.lastFetched }} <span v-if="loading">{{ strings.loading }}</span
> <span v-if="!loading && !lastUpdate">{{ strings.never }}</span
> <NcDateTime v-if="!loading && lastUpdate" :timestamp="lastUpdate.valueOf()" />
</div>
</div>
</div>
<div class="submit-buttons">
<NcButton native-type="submit">{{ strings.save }}</NcButton
class="folder-button"
>{{ strings.pickFolder }}</NcButton
>
</div>
</form>
</div>
</NcAppSettingsSection
>
<div class="submit-buttons">
<NcButton type="submit" :disabled="loading">{{ strings.save }}</NcButton
>
</div>
</form>
</section>
</NcAppSettingsSection
>
</div>
</template>
<script>
import NcAppSettingsSection from '@nextcloud/vue/components/NcAppSettingsSection'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import axios from '@nextcloud/axios'
import { t, n } from '@nextcloud/l10n'
import { parseISO as parseDate } from 'date-fns/parseISO'
import { format as formatDate } from 'date-fns/format'
import NcButton from '@nextcloud/vue/components/NcButton'
import { settingsAxios } from './axios'
import { t } from '@nextcloud/l10n'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/style.css'
export default {
name: 'App',
name: 'JukeboxUserSettings',
components: {
NcAppSettingsSection,
NcButton,
NcDateTime,
NcNoteCard,
NcSelect,
NcTextField,
NcButton,
},
data() {
return {
loading: true,
interval: null,
lastUpdate: null,
intervalOptions: [
{ label: t('jukebox', 'Every hour'), value: 1 },
{ label: n('jukebox', 'Every %n hour', 'Every %n hours', 3), value: 3 },
{ label: n('jukebox', 'Every %n hour', 'Every %n hours', 6), value: 6 },
{ label: n('jukebox', 'Every %n hour', 'Every %n hours', 9), value: 9 },
{ label: n('jukebox', 'Every %n hour', 'Every %n hours', 12), value: 12 },
{
label: n('jukebox', 'Every %n hour (default)', 'Every %n hours (default)', 24),
value: 24,
},
],
supportedCurrencies: [],
currencySearch: '',
loading: false,
musicFolder: '',
strings: {
info: t(
'jukebox',
'To make sure your currencies are found for the rates to be updated, please ensure your ' +
'currencies are named appropriately.',
),
requirements: t(
'jukebox',
'Currency names must contain {bStart}at least one of{bEnd}:',
{ bStart: '<b>', bEnd: '</b>' },
undefined,
{ escape: false },
),
requirementsList: [
t(
'jukebox',
'The currency symbol - e.g. {cStart}${cEnd}, {cStart}€{cEnd}, {cStart}£{cEnd}',
{ cStart: '<code>', cEnd: '</code>' },
undefined,
{ escape: false },
),
t(
'jukebox',
'The currency code - e.g. {cStart}USD{cEnd}, {cStart}EUR{cEnd}, {cStart}GBP{cEnd} (case-insensitive)',
{ cStart: '<code>', cEnd: '</code>' },
undefined,
{ escape: false },
),
],
infoNote: t(
'jukebox',
'The naming rules apply for both main &amp; additional currencies.',
undefined,
undefined,
{ escape: false },
),
cronSettingsHeader: t('jukebox', 'Cron Settings'),
exampleHeader: t('jukebox', 'Example names:'),
supportedCurrencies: t('jukebox', 'Supported currencies:'),
currencySearchLabel: t('jukebox', 'Search'),
currencySearchPlaceholder: t('jukebox', 'e.g. $, USD, US Dollar'),
intervalLabel: t('jukebox', 'Currency conversion rate update interval'),
tableSymbol: t('jukebox', 'Symbol'),
tableCode: t('jukebox', 'Code'),
tableName: t('jukebox', 'Name'),
fetchNow: t('jukebox', 'Fetch Rates Now'),
lastFetched: t('jukebox', 'Rates last fetched:'),
loading: t('jukebox', 'Loading…'),
never: t('jukebox', 'Never'),
header: t('jukebox', 'Jukebox'),
musicLibrarySettings: t('jukebox', 'Music Library'),
musicFolderLabel: t('jukebox', 'Music Folder Path'),
musicFolderPlaceholder: t('jukebox', 'e.g. Music'),
pickFolder: t('jukebox', 'Pick a folder'),
save: t('jukebox', 'Save'),
},
}
@@ -224,93 +75,67 @@ export default {
},
methods: {
async fetchSettings() {
this.loading = true
try {
this.loading = true
const resp = await axios.get('/cron')
const data = resp.data.ocs.data
const response = await settingsAxios.get('/settings')
const data = response.data.ocs.data
this.musicFolder = data.music_folder_path || ''
} catch (e) {
console.error('Failed to fetch settings:', e)
} finally {
this.loading = false
console.debug('[DEBUG] Jukebox settings fetched', data)
const interval = this.getIntervalByValue(data.interval)
if (interval) {
console.debug('[DEBUG] Interval found', interval)
this.interval = interval.label
} else {
console.warn('Invalid interval value', data.interval)
}
if (data.last_update) {
const lastUpdate = parseDate(data.last_update, new Date())
this.lastUpdate = lastUpdate
}
this.supportedCurrencies = data.supported_currencies.sort((a, b) =>
a.code.localeCompare(b.code),
)
} catch (e) {
console.error('Failed to fetch Jukebox settings', e)
}
},
getIntervalByValue(value) {
return this.intervalOptions.find((x) => x.value === value)
},
getIntervalByLabel(label) {
return this.intervalOptions.find((x) => x.label === label)
},
async doCron() {
try {
const resp = await axios.post('/cron/run')
const data = resp.data.ocs.data
console.debug('[DEBUG] Cron executed', data)
this.fetchSettings()
} catch (e) {
console.error('Failed to run cron', e)
}
},
clearCurrencySearch() {
this.currencySearch = ''
},
async save() {
this.loading = true
try {
this.loading = true
const interval = this.getIntervalByLabel(this.interval)?.value ?? 24
const resp = await axios.put('/cron', { data: { interval } })
const data = resp.data.ocs.data
this.loading = false
console.debug('[DEBUG] Jukebox settings saved', data)
this.fetchSettings()
const data = {
music_folder_path: this.musicFolder,
}
console.log('Saving settings :', data)
await settingsAxios.put('/settings', { data })
} catch (e) {
console.error('Failed to update Jukebox settings', e)
console.error('Failed to save settings:', e)
} finally {
this.loading = false
}
},
},
computed: {
intervals() {
return this.intervalOptions.map((x) => x.label)
},
currencies() {
if (!this.supportedCurrencies) {
return []
}
if (!this.currencySearch) {
return this.supportedCurrencies
}
async openFolderPicker() {
try {
const picker = getFilePickerBuilder(this.strings.musicFolderLabel)
.allowDirectories(true)
.addButton({
label: t('jukebox', 'Select'),
callback: (nodes) => {
console.log('Selected nodes:', nodes)
const node = nodes?.[0]
if (!node || !node._data?.root || !node._data?.attributes?.filename) return
const root = node._data.root
const fullPath = node._data.attributes.filename
this.musicFolder = fullPath.startsWith(root)
? fullPath.slice(root.length) || '/'
: fullPath
if (this.musicFolder.startsWith('/')) {
this.musicFolder = this.musicFolder.slice(1)
}
console.log('Selected folder path:', this.musicFolder)
},
})
.build()
return this.supportedCurrencies.filter((currency) => {
return [
currency.symbol.toLowerCase().includes(this.currencySearch.toLowerCase()),
currency.code.toLowerCase().includes(this.currencySearch.toLowerCase()),
currency.name.toLowerCase().includes(this.currencySearch.toLowerCase()),
].some(Boolean)
})
await picker.pick()
} catch (e) {
if (e.message.includes('No nodes selected')) return
console.error('Failed to open folder picker:', e)
}
},
},
}
</script>
<style scoped lang="scss">
#jukebox-content {
h2:first-child {
<style scoped>
#jukebox-user-settings {
h2 {
margin-top: 0;
}
@@ -318,64 +143,14 @@ export default {
margin-top: 16px;
}
.cron-flex {
.input-with-button {
display: flex;
align-items: start;
gap: 24px;
}
.cron-last-update-container {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
p {
margin: 0.5em 0;
}
ol {
padding-left: 2.5em;
}
ul {
padding-left: 1em;
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--color-border);
tr:not(:last-child),
thead tr {
border-bottom: 1px solid var(--color-border);
}
tbody {
display: block;
max-height: 300px;
overflow-y: scroll;
}
thead,
tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
td,
th {
padding: 4px 8px;
}
}
.currency-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 2em;
.folder-button {
flex-shrink: 0;
}
}
</style>

7
src/axios.ts Normal file
View File

@@ -0,0 +1,7 @@
import { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
const baseURL = generateOcsUrl('/apps/jukebox/api')
export const settingsAxios = axios.create({
baseURL,
})

View File

@@ -1,12 +1,8 @@
import { settingsAxios } from './axios'
import Settings from './Settings.vue'
import './style.scss'
import { createApp } from 'vue'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
const baseURL = generateOcsUrl('/apps/jukebox/api')
axios.defaults.baseURL = baseURL
console.log('[DEBUG] Mounting jukebox Settings')
console.log('[DEBUG] Base URL:', baseURL)
console.log('[DEBUG] Base URL:', settingsAxios.defaults.baseURL)
createApp(Settings).mount('#jukebox-settings')

1
version.txt Normal file
View File

@@ -0,0 +1 @@
0.1.0

View File

@@ -15,8 +15,15 @@ export default createAppConfig(
cssCodeSplit: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('@nextcloud/dialogs')) return 'nextcloud-dialogs'
if (id.includes('@nextcloud/vue')) return 'nextcloud-vue'
if (id.includes('vue')) return 'vue'
if (id.includes('vue-router')) return 'vue-router'
if (id.includes('axios')) return 'axios'
return 'vendor' // fallback for other deps
}
},
},
},