Compare commits

..

No commits in common. "f246d2c1b5bd330cddd4040245af049d10855224" and "ce1dc19ff9d8e33fb6c4285152e2b9fa14223704" have entirely different histories.

25 changed files with 253 additions and 2297 deletions

9
.eslintrc.cjs Normal file

@ -0,0 +1,9 @@
module.exports = {
extends: ['@nextcloud/eslint-config/vue3', 'plugin:prettier/recommended'],
rules: {
'jsdoc/require-jsdoc': 'off',
'vue/first-attribute-linebreak': 'off',
'sort-imports': 'error',
'vue/attributes-order': ['error', { alphabetical: true }],
},
}

@ -1,4 +1,4 @@
name: opds_catalog
name: app_template
on: [push]
jobs:
@ -26,6 +26,24 @@ jobs:
- run: composer run cs:check
- run: composer run phpstan
nodejs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: skjnldsv/read-package-engines-version-actions@v3
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^10'
- uses: actions/setup-node@v4
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- run: npm ci
- run: npm run lint
- run: npm run stylelint
- run: npm run build
release:
if: gitea.ref_type == 'tag'
runs-on: ubuntu-latest
@ -36,6 +54,15 @@ jobs:
- uses: actions/checkout@v4
- run: curl -sSLo /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar
- run: chmod +x /usr/local/bin/composer
- uses: skjnldsv/read-package-engines-version-actions@v3
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^10'
- uses: actions/setup-node@v4
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- run: make dist
- uses: akkuman/gitea-release-action@v1
with:

14
.l10nignore Normal file

@ -0,0 +1,14 @@
.idea/
*.iml
vendor/
vendor-bin/*/vendor/
.php-cs-fixer.cache
tests/.phpunit.cache
node_modules/
js/
css/
build/

1
.nvmrc Normal file

@ -0,0 +1 @@
20

@ -1,46 +1,12 @@
## 0.8.8 - 2018-01-31
### Changed
- some minor changes to the preference sections
# Changelog
## 0.8.5 - 0.8.7
### Changed
- debugging NC and OC package signing collision
All notable changes to this project will be documented in this file.
## 0.8.4 - 2018-01-19
### Changed
- signed package for publication in Owncloud marketplace
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).
## 0.8.3 - 2018-01-18
### Changed
- increased maximum version for OC and NC
## [Unreleased]
## 0.8.2 - 2017-01-19
### Fixed
- Support login tokens ('app passwords', NC/OC) and 2FA (NC)
## 0.8.1 - 2017-01-14
### Changed
- more robust preview generator, fallback to mimetype icon when showPreview throws exception
## 0.8.0 - 2017-01-14
### New
- FictionBook 2 (.fb2) metadata parser
- FB2 preview provider
## 0.7.3 - 2017-01-09
### Fixed
- XML error after deleting an epub file from Library
- [#23](https://github.com/Yetangitu/owncloud-apps/issues/23)
## 0.7.2 - 2017-01-09
### Changed
- Modified info.xml, now with working screenshot url...
## 0.7.1 - 2017-01-09
### Added
- Modified info.xml, added screenshot
## 0.7.0 - 2017-01-09
### Changed
- New logo
- First release to be compatible with Nextcloud
- First release

@ -1,6 +1,6 @@
FROM nextcloud:30
ARG APP_NAME=opds_catalog
ARG APP_NAME=app_template
ENV NEXTCLOUD_UPDATE=1
ENV NEXTCLOUD_ADMIN_USER=$APP_NAME
ENV NEXTCLOUD_ADMIN_PASSWORD=$APP_NAME
@ -8,7 +8,7 @@ ENV NEXTCLOUD_INIT_HTACCESS=1
ENV SQLITE_DATABASE=$APP_NAME
RUN apt-get update && \
apt-get install -y sqlite3 && \
apt-get install -y nodejs npm sqlite3 && \
rm -f /usr/local/etc/php/conf.d/opcache-recommended.ini && \
/entrypoint.sh true

@ -19,6 +19,9 @@ build:
ifneq (,$(wildcard $(CURDIR)/composer.json))
make composer
endif
ifneq (,$(wildcard $(CURDIR)/package.json))
make npm
endif
# Installs and updates the composer dependencies. If composer is not installed
# a copy is fetched from the web
@ -34,6 +37,12 @@ else
composer install --prefer-dist
endif
# Installs npm dependencies
.PHONY: npm
npm:
npm ci
npm run build
# Removes the appstore build
.PHONY: clean
clean:
@ -44,6 +53,9 @@ clean:
.PHONY: distclean
distclean: clean
rm -rf vendor
rm -rf node_modules
rm -rf js/vendor
rm -rf js/node_modules
# Builds the source and appstore package
.PHONY: dist
@ -59,7 +71,10 @@ source:
tar -C .. -cvzf $(source_package_name).tar.gz \
--exclude-vcs \
--exclude="$(app_name)/build" \
--exclude="$(app_name)/js/node_modules" \
--exclude="$(app_name)/node_modules" \
--exclude="$(app_name)/*.log" \
--exclude="$(app_name)/js/*.log" \
$(app_name)
# Builds the source package for the app store, ignores php tests, js tests
@ -68,12 +83,14 @@ source:
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(appstore_build_directory)
composer install --prefer-dist --no-dev
tar -C .. -cvzf $(appstore_package_name).tar.gz \
$(app_name)/appinfo \
$(app_name)/css \
$(app_name)/img \
$(app_name)/js \
$(app_name)/l10n \
$(app_name)/lib \
$(app_name)/vendor \
$(app_name)/templates \
$(app_name)/CHANGELOG.md
# Start a nextcloud server on Docker to kickstart developement
@ -83,3 +100,18 @@ dev: build
docker rm $(app_name) || true
docker build -t $(app_name) .
docker run -itd --rm --name $(app_name) -v $(CURDIR):/var/www/html/apps/$(app_name) -p 80:80 $(app_name)
npm run watch || docker stop $(app_name)
# Generate translations
.PHONY: l10n
l10n:
docker run --rm \
-v $(CURDIR):/app \
--entrypoint php \
nextcloudci/translations \
/translationtool.phar create-pot-files
docker run --rm \
-v $(CURDIR):/app \
--entrypoint php \
nextcloudci/translations \
/translationtool.phar convert-po-files

@ -1,18 +1,27 @@
<?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>opds_catalog</id>
<id>app_template</id>
<name>App Template</name>
<summary>An example summary</summary>
<description>An example description</description>
<version>1.0.0</version>
<licence>agpl</licence>
<author mail="example@example.com" homepage="https://example.com">Example</author>
<namespace>OpdsCatalog</namespace>
<namespace>AppTemplate</namespace>
<category>customization</category>
<bugs>https://example.com/bugs</bugs>
<dependencies>
<php min-version="8.1"/>
<nextcloud min-version="29" max-version="31"/>
</dependencies>
<navigations>
<navigation>
<id>app_template</id>
<name>App Template</name>
<route>app_template.page.index</route>
<icon>app.svg</icon>
<type>link</type>
</navigation>
</navigations>
</info>

@ -1,19 +1,19 @@
{
"name": "nextcloud/opds_catalog",
"name": "nextcloud/app_template",
"description": "An example description",
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "example",
"email": "example@example.com",
"homepage": "https://example.com"
}
],
"autoload": {
"psr-4": {
"OCA\\OpdsCatalog\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"OCA\\AppTemplate\\": "lib/",
"OCP\\": "vendor/nextcloud/ocp/OCP/"
},
"files": [
"stubs/OC_Image.php"
]
}
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
@ -23,16 +23,14 @@
"rector": "rector && composer cs:fix"
},
"require": {
"php": "^8.1",
"kiwilan/php-ebook": "^3.0.04",
"kiwilan/php-opds": "^2.1.0"
"php": "^8.1"
},
"require-dev": {
"nextcloud/ocp": "^30.0.6",
"roave/security-advisories": "dev-latest",
"nextcloud/coding-standard": "^1.3.2",
"nextcloud/rector": "^0.3.1",
"phpstan/phpstan": "~1.12.19",
"phpstan/phpstan": "~1.12.18",
"rector/rector": "~1.2.10"
},
"config": {

2010
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -2,12 +2,8 @@
declare(strict_types=1);
namespace OCA\OpdsCatalog\AppInfo;
namespace OCA\AppTemplate\AppInfo;
use OCA\OpdsCatalog\Provider\AmazonPreviewProvider;
use OCA\OpdsCatalog\Provider\ComicBookPreviewProvider;
use OCA\OpdsCatalog\Provider\EpubPreviewProvider;
use OCA\OpdsCatalog\Provider\FictionBookPreviewProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@ -15,18 +11,13 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext;
class Application extends App implements IBootstrap
{
public const APP_ID = 'opds_catalog';
public const APP_ID = 'app_template';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void {
$context->registerPreviewProvider(AmazonPreviewProvider::class, '/application\/(vnd.amazon.(ebook|mobi8-ebook)|x-(mobi8|mobipocket)-ebook)/');
$context->registerPreviewProvider(ComicBookPreviewProvider::class, '/application\/(vnd.comicbook[+-](rar|zip)|x-cb[7rtz])/');
$context->registerPreviewProvider(EpubPreviewProvider::class, '/application\/epub\+zip/');
$context->registerPreviewProvider(FictionBookPreviewProvider::class, '/application\/x-fictionbook.*/');
}
public function register(IRegistrationContext $context): void {}
public function boot(IBootContext $context): void {}
}

@ -2,120 +2,26 @@
declare(strict_types=1);
namespace OCA\OpdsCatalog\Controller;
namespace OCA\AppTemplate\Controller;
use Kiwilan\Ebook\Ebook;
use Kiwilan\Opds\Entries\OpdsEntryBook;
use Kiwilan\Opds\Entries\OpdsEntryBookAuthor;
use Kiwilan\Opds\Entries\OpdsEntryNavigation;
use Kiwilan\Opds\Enums\OpdsVersionEnum;
use Kiwilan\Opds\Opds;
use Kiwilan\Opds\OpdsConfig;
use Kiwilan\Opds\OpdsResponse;
use OCA\OpdsCatalog\AppInfo\Application;
use OCA\AppTemplate\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\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\TemplateResponse;
class PageController extends Controller
{
public function __construct(
IRequest $request,
private readonly IRootFolder $rootFolder,
private readonly IUser $user,
private readonly IURLGenerator $urlGenerator
) {
parent::__construct(Application::APP_ID, $request);
}
#[NoCSRFRequired]
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/index/{path}', requirements: ['path' => '.*'])]
public function index(string $path): Response {
$userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
$root = $userFolder->get($path);
$feeds = [];
if ($root instanceof Folder) {
foreach ($root->getDirectoryListing() as $node) {
if ($node instanceof Folder) {
$feeds[] = new OpdsEntryNavigation(
(string) $node->getId(),
$node->getName(),
$this->urlGenerator->linkToRouteAbsolute(
'opds_catalog.page.index',
['path' => $userFolder->getRelativePath($node->getPath()) ?? $path.'/'.$node->getName()]
),
properties: [
'numberOfItems' => count($node->getDirectoryListing()),
],
updated: new \DateTime('@'.$node->getMTime())
);
} elseif ($node instanceof File) {
$ebook = Ebook::read($node->getPath());
if ($ebook instanceof Ebook) {
$authors = [];
foreach ($ebook->getAuthors() as $author) {
if (null !== $author->getName()) {
$authors[] = new OpdsEntryBookAuthor($author->getName());
}
}
$feeds[] = new OpdsEntryBook(
(string) $node->getId(),
$ebook->getTitle() ?? $ebook->getFilename(),
$this->urlGenerator->linkToRoute('opds_catalog.page.books', ['path' => $path.'/'.$node->getName()]),
summary: $ebook->getDescription(),
media: $this->urlGenerator->linkTo('', 'core/preview', ['fileId' => $node->getId(), 'x' => 1024, 'y' => 1024]),
updated: new \DateTime('@'.$node->getMTime()),
download: $this->urlGenerator->linkTo('', 'remote.php/dav/files/'.$this->user->getUID().'/'.$userFolder->getRelativePath($node->getPath())),
mediaThumbnail: $this->urlGenerator->linkTo('', 'core/preview', ['fileId' => $node->getId()]),
categories: $ebook->getTags(),
authors: $authors,
published: $ebook->getPublishDate(),
volume: $ebook->getVolume(),
serie: $ebook->getSeries(),
language: $ebook->getLanguage(),
identifier: $ebook->getIdentifiers()[0]->getValue(),
publisher: $ebook->getPublisher()
);
}
}
}
}
$config = new OpdsConfig(
name: $this->user->getDisplayName()."'s Library",
author: $this->user->getDisplayName(),
iconUrl: $this->urlGenerator->getAbsoluteURL('/avatar/'.$this->user->getUID().'/512'),
startUrl: $this->urlGenerator->linkToRouteAbsolute('opds_catalog.page.index')
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
#[FrontpageRoute(verb: 'GET', url: '/')]
public function index(): TemplateResponse {
return new TemplateResponse(
Application::APP_ID,
'index',
);
$opds = Opds::make($config)->feeds($feeds)->get();
$response = $opds->getResponse();
if ($response instanceof OpdsResponse) {
if (OpdsVersionEnum::v1Dot2 === $opds->getVersion()) {
// @phpstan-ignore-next-line
return new DataResponse(data: $response->getContents(), headers: $response->getHeaders());
}
if (OpdsVersionEnum::v2Dot0 === $opds->getVersion()) {
// @phpstan-ignore-next-line
return new JSONResponse(data: $response->getJson(), headers: $response->getHeaders());
}
}
return new NotFoundResponse();
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\OpdsCatalog\Provider;
class AmazonPreviewProvider extends EbookPreviewProvider
{
public function getMimeType(): string {
return '/application\/(vnd.amazon.(ebook|mobi8-ebook)|x-(mobi8|mobipocket)-ebook)/';
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\OpdsCatalog\Provider;
class ComicBookPreviewProvider extends EbookPreviewProvider
{
public function getMimeType(): string {
return '/application\/(vnd.comicbook[+-](rar|zip)|x-cb[7rtz])/';
}
}

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\OpdsCatalog\Provider;
use Kiwilan\Ebook\Ebook;
use Kiwilan\Ebook\EbookCover;
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\IImage;
use OCP\Image;
use OCP\Preview\IProviderV2;
abstract class EbookPreviewProvider implements IProviderV2
{
public function isAvailable(FileInfo $file): bool {
return Ebook::isValid($file->getPath());
}
public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
$ebook = Ebook::read($file->getPath());
if ($ebook instanceof Ebook) {
$cover = $ebook->getCover();
if ($cover instanceof EbookCover) {
$path = $cover->getPath();
if (null !== $path) {
$image = new Image();
$image->loadFromFile($path);
$image->scaleDownToFit($maxX, $maxY);
return $image;
}
}
}
return null;
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\OpdsCatalog\Provider;
class EpubPreviewProvider extends EbookPreviewProvider
{
public function getMimeType(): string {
return '/application\/epub\+zip/';
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\OpdsCatalog\Provider;
class FictionBookPreviewProvider extends EbookPreviewProvider
{
public function getMimeType(): string {
return '/application\/x-fictionbook.*/';
}
}

40
package.json Normal file

@ -0,0 +1,40 @@
{
"name": "app_template",
"license": "AGPL-3.0-or-later",
"engines": {
"node": "^20.0.0",
"npm": "^10.0.0"
},
"scripts": {
"build": "vite build",
"dev": "vite --mode development build",
"watch": "vite --mode development build --watch",
"lint": "vue-tsc && eslint src",
"lint:fix": "vue-tsc && eslint src --fix",
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css",
"stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix"
},
"type": "module",
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@formatjs/intl-segmenter": "^11.7.9",
"@nextcloud/vite-config": "^2.3.1",
"@nextcloud/vue": "9.0.0-alpha.6",
"vite": "^6.1.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/eslint-config": "^8.4.2",
"@nextcloud/prettier-config": "^1.1.0",
"@nextcloud/stylelint-config": "^3.0.1",
"@vue/tsconfig": "^0.7.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"typescript": "~5.5.4",
"vue-tsc": "^2.2.2"
},
"prettier": "@nextcloud/prettier-config"
}

26
src/App.vue Normal file

@ -0,0 +1,26 @@
<template>
<NcAppContent>
<div id="app_template">
<h1>Hello world!</h1>
</div>
</NcAppContent>
</template>
<script>
import { NcAppContent } from '@nextcloud/vue'
export default {
name: 'App',
components: {
NcAppContent,
},
}
</script>
<style scoped lang="scss">
#app_template {
display: flex;
justify-content: center;
margin: 16px;
}
</style>

6
src/main.ts Normal file

@ -0,0 +1,6 @@
import '@formatjs/intl-segmenter/polyfill'
import App from './App.vue'
import { createApp } from 'vue'
const View = createApp(App)
View.mount('#app_template')

@ -1,20 +0,0 @@
<?php
use OCP\IImage;
interface OC_Image extends IImage
{
/**
* Loads an image from a local file.
*
* @param bool|string $imagePath the path to a local file
*
* @return bool|GdImage An image resource or false on error
*/
public function loadFromFile($imagePath = false);
/**
* Shrinks larger images to fit within specified boundaries while preserving ratio.
*/
public function scaleDownToFit(int $maxWidth, int $maxHeight): bool;
}

3
stylelint.config.cjs Normal file

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

13
templates/index.php Normal file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
use OCA\AppTemplate\AppInfo\Application;
use OCP\Util;
Util::addScript(Application::APP_ID, Application::APP_ID.'-main');
Util::addStyle(Application::APP_ID, Application::APP_ID.'-main');
?>
<div id="app_template"></div>

15
tsconfig.json Normal file

@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig",
"include": ["./src/**/*.ts", "./src/**/*.vue", "**/*.ts"],
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noImplicitAny": false,
"rootDir": ".",
"strict": true,
"noEmit": true,
}
}

20
vite.config.ts Normal file

@ -0,0 +1,20 @@
import { join, resolve } from 'path'
import { createAppConfig } from '@nextcloud/vite-config'
import { defineConfig } from 'vite'
const config = defineConfig({
build: {
sourcemap: false,
},
})
export default createAppConfig(
{
main: resolve(join('src', 'main.ts')),
},
{
config,
createEmptyCSSEntryPoints: true,
thirdPartyLicense: false,
},
)