mirror of
https://github.com/ad-aures/castopod.git
synced 2026-04-08 17:26:43 +02:00
Compare commits
128 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc041702dd |
||
|
|
c13bbdffdf |
||
|
|
aad17646f1 |
||
|
|
385a3cb13a |
||
|
|
ed57e13b40 |
||
|
|
6b302ad8bf |
||
|
|
cc86ce030f |
||
|
|
3943441683 |
||
|
|
b747967a18 |
||
|
|
a585362827 |
||
|
|
abf214757c |
||
|
|
f01de13637 |
||
|
|
77826552f1 |
||
|
|
e5fb676cb6 |
||
|
|
49a43d08cc | ||
|
|
89d0fe4a7e | ||
|
|
950d42c838 | ||
|
|
6be7a1f4d7 | ||
|
|
4c46c15e39 | ||
|
|
bbfaa1bfc3 | ||
|
|
85503ee282 | ||
|
|
265cbbac09 | ||
|
|
e291b6239c | ||
|
|
40f671c8b6 | ||
|
|
3d0db5c64a | ||
|
|
b5a403b990 | ||
|
|
835f099f2e | ||
|
|
9dffc8d5f1 | ||
|
|
8ec42c33ff | ||
|
|
346c00e7b5 | ||
|
|
96b2df15b0 | ||
|
|
61d6a6b60f | ||
|
|
00870ceff2 | ||
|
|
31fee52208 | ||
|
|
567d5e01a3 | ||
|
|
94cea0ce91 | ||
|
|
0e4e301b81 | ||
|
|
f8fb25f52d | ||
|
|
93b4741333 | ||
|
|
5578104207 | ||
|
|
5dce8cb949 | ||
|
|
1e6477db67 | ||
|
|
0265775177 | ||
|
|
c9fabe8888 | ||
|
|
6934c8aa8f | ||
|
|
70c97971fc | ||
|
|
9f74cca342 | ||
|
|
f295e9aa4c | ||
|
|
fc2e7a0d83 | ||
|
|
f981937645 | ||
|
|
f288a750f5 | ||
|
|
7e8f0003d1 | ||
|
|
888d610c2d | ||
|
|
775b302f7c | ||
|
|
09256b4eb7 | ||
|
|
0736050d1a | ||
|
|
a90cdfdcdb | ||
|
|
8402cc29d2 | ||
|
|
08c7df2a5d | ||
|
|
34be5bccab | ||
|
|
d3a98db6d0 | ||
|
|
00bd4c02ee | ||
|
|
85704bfbe0 | ||
|
|
8cf9c6dc83 | ||
|
|
b869acb3a9 | ||
|
|
11ccd0ebe7 | ||
|
|
f50098ec89 | ||
|
|
77e55835c0 | ||
|
|
fa6967e65c | ||
|
|
ea720e01ba | ||
|
|
cbf739e95c | ||
|
|
63f93f585b | ||
|
|
65d74f14e6 | ||
|
|
1667f5b202 | ||
|
|
1d7583d738 | ||
|
|
d30c49cdff | ||
|
|
a83afb0004 | ||
|
|
732d42923d | ||
|
|
63c763f941 | ||
|
|
a68959c906 | ||
|
|
74f9325946 | ||
|
|
2b1bbf3430 | ||
|
|
37ee6d35b4 | ||
|
|
3cd30205d9 | ||
|
|
53232d3b61 | ||
|
|
7405f8897d | ||
|
|
fc9ea7597e | ||
|
|
fee7905935 | ||
|
|
1a439083a2 | ||
|
|
0ba0a25b11 | ||
|
|
c21864ee25 | ||
|
|
1c5fe1fea6 | ||
|
|
a8c81b3fa1 | ||
|
|
322836254e | ||
|
|
e9c04548de | ||
|
|
5d35524875 | ||
|
|
7a8cd4c730 | ||
|
|
5339669ea6 | ||
|
|
0eeedb9dc6 | ||
|
|
827522643e | ||
|
|
e417d45b14 | ||
|
|
cc6495dc7c | ||
|
|
8f8c61eaae | ||
|
|
91dc8c8325 | ||
|
|
3a900bbab6 | ||
|
|
2035c39fd1 | ||
|
|
d7b9730d7e | ||
|
|
b5bd2db28f | ||
|
|
e2a90def88 | ||
|
|
014facd5a1 | ||
|
|
80d2c48ee2 | ||
|
|
8ec79097bb | ||
|
|
45ac2a4be9 | ||
|
|
b62b483ad9 | ||
|
|
6f833fc76a | ||
|
|
82714e7155 | ||
|
|
dfb7888aeb | ||
|
|
e6bfdfc390 | ||
|
|
1510e36c0a | ||
|
|
b5eddf351f | ||
|
|
896f00661f | ||
|
|
9a80de4068 | ||
|
|
89ac92fb41 | ||
|
|
3d8aedf9c3 | ||
|
|
e80a33bf2a | ||
|
|
27d2a1b0ff | ||
|
|
587938d2bf | ||
|
|
7253e13ac2 |
890 changed files with 18802 additions and 16177 deletions
|
|
@ -3,17 +3,15 @@
|
|||
"projectOwner": "adaures",
|
||||
"repoType": "gitlab",
|
||||
"repoHost": "https://code.castopod.org",
|
||||
"files": ["README.md", "docs/src/content/docs/en/index.mdx"],
|
||||
"files": ["README.md"],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributorsPerLine": 7,
|
||||
"wrapperTemplate": "\n<table class=\"all-contributors-table\">\n <tbody><%= bodyContent %> </tbody>\n<%= tableFooterContent %></table>\n\n",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "yassinedoghri",
|
||||
"name": "Yassine Doghri",
|
||||
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/3/avatar.png",
|
||||
"profile": "https://github.com/yassinedoghri",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11021441?v=4",
|
||||
"profile": "https://yassinedoghri.com",
|
||||
"contributions": [
|
||||
"code",
|
||||
"bug",
|
||||
|
|
@ -472,18 +470,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ghose",
|
||||
"name": "ghose (XoseM)",
|
||||
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/12617257/large/a201650da44fed28890b0e0d8477a663.jpg",
|
||||
"profile": "https://crowdin.com/profile/xosem",
|
||||
"contributions": [
|
||||
{
|
||||
"type": "translation",
|
||||
"url": "https://translate.castopod.org"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "basen1982",
|
||||
"name": "Andreas Olsson",
|
||||
|
|
@ -545,7 +531,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"login": "ahmed.sabouni11",
|
||||
"login": "ahmedsabouni",
|
||||
"name": "Ahmed Sabouni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
|
||||
"profile": "https://github.com/ahmedsabouni",
|
||||
|
|
@ -564,11 +550,25 @@
|
|||
"contributions": ["code"]
|
||||
},
|
||||
{
|
||||
"login": "NeoluxConsulting",
|
||||
"login": "Dwev",
|
||||
"name": "Guy Martin",
|
||||
"avatar_url": "https://secure.gravatar.com/avatar/6e745565356330c1e29a85d52bffdaa1?s=80&d=identicon",
|
||||
"profile": "https://code.castopod.org/NeoluxConsulting",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/46626050?v=4",
|
||||
"profile": "https://github.com/Dwev",
|
||||
"contributions": ["bug", "code"]
|
||||
},
|
||||
{
|
||||
"login": "prcutler",
|
||||
"name": "Paul Cutler",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/67276?v=4",
|
||||
"profile": "https://github.com/prcutler",
|
||||
"contributions": ["doc", "question", "ideas"]
|
||||
},
|
||||
{
|
||||
"login": "nateritter",
|
||||
"name": "Nate Ritter",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/198798?v=4",
|
||||
"profile": "https://github.com/nateritter",
|
||||
"contributions": ["code"]
|
||||
}
|
||||
],
|
||||
"commitConvention": "none"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# ⚠️ NOT optimized for production
|
||||
# should be used only for development purposes
|
||||
#---------------------------------------------------
|
||||
FROM php:8.2-fpm
|
||||
FROM php:8.5-fpm
|
||||
|
||||
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
|
||||
|
||||
|
|
@ -47,11 +47,4 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|||
&& docker-php-ext-enable redis \
|
||||
# mysqli for database access
|
||||
&& docker-php-ext-install mysqli \
|
||||
&& docker-php-ext-enable mysqli \
|
||||
# configure php
|
||||
&& echo "file_uploads = On\n" \
|
||||
"memory_limit = 512M\n" \
|
||||
"upload_max_filesize = 500M\n" \
|
||||
"post_max_size = 512M\n" \
|
||||
"max_execution_time = 300\n" \
|
||||
> /usr/local/etc/php/conf.d/uploads.ini
|
||||
&& docker-php-ext-enable mysqli
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "composer install && pnpm install && pnpm run build:static && php spark migrate --all && php spark db:seed DevSeeder",
|
||||
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0",
|
||||
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0 --port ${APP_PORT:-8080}",
|
||||
"postAttachCommand": "crontab .devcontainer/crontab && service cron reload",
|
||||
"shutdownAction": "stopCompose",
|
||||
"features": {
|
||||
|
|
@ -30,7 +30,17 @@
|
|||
"spark": "php",
|
||||
"env": "dotenv",
|
||||
".rsync-filter": "diff"
|
||||
}
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"plugins/**/manifest.json",
|
||||
"tests/modules/Plugins/mocks/manifests/*.json",
|
||||
"tests/modules/Plugins/mocks/plugins/**/manifest.json"
|
||||
],
|
||||
"url": "/workspaces/castopod/modules/Plugins/Manifest/manifest.schema.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extensions": [
|
||||
"astro-build.astro-vscode",
|
||||
|
|
@ -52,7 +62,8 @@
|
|||
"stylelint.vscode-stylelint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"yzhang.markdown-all-in-one"
|
||||
"yzhang.markdown-all-in-one",
|
||||
"42Crunch.vscode-openapi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||
environment:
|
||||
APP_PORT: ${APP_PORT:-8080} # used in devcontainer.json file
|
||||
VITE_PORT: ${VITE_PORT:-5173} # used in ../vite.config.js file
|
||||
CI_ENVIRONMENT: development
|
||||
vite_environment: development
|
||||
app_forceGlobalSecureRequests: 0 #false
|
||||
app_baseURL: http://localhost:8080/
|
||||
media_baseURL: http://localhost:8080/
|
||||
app_baseURL: http://localhost:${APP_PORT:-8080}/
|
||||
media_baseURL: http://localhost:${APP_PORT:-8080}/
|
||||
admin_gateway: cp-admin
|
||||
auth_gateway: cp-auth
|
||||
analytics_salt: dev_analytics_salt
|
||||
|
|
@ -29,16 +28,10 @@ services:
|
|||
email_SMTPHost: mailpit
|
||||
email_SMTPUser: castopod
|
||||
email_SMTPPass: castopod
|
||||
email_SMTPPort: 1025
|
||||
email_SMTPPort: ${MAILPIT_SMTP_PORT:-1025}
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.2
|
||||
volumes:
|
||||
|
|
@ -69,8 +62,8 @@ services:
|
|||
volumes:
|
||||
- mailpit:/data
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
- ${MAILPIT_WEBUI_PORT:-8025}:8025
|
||||
- ${MAILPIT_SMTP_PORT:-1025}:1025
|
||||
environment:
|
||||
MP_MAX_MESSAGES: 5000
|
||||
MP_DATA_FILE: /data/mailpit.db
|
||||
|
|
@ -78,7 +71,6 @@ services:
|
|||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||
|
||||
volumes:
|
||||
redis:
|
||||
mariadb:
|
||||
phpmyadmin:
|
||||
mailpit:
|
||||
|
|
|
|||
5
.devcontainer/uploads.ini
Normal file
5
.devcontainer/uploads.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
file_uploads = On
|
||||
memory_limit = 512M
|
||||
upload_max_filesize = 500M
|
||||
post_max_size = 512M
|
||||
max_execution_time = 300
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -179,3 +179,9 @@ modules/Admin/Language/*/PersonsTaxonomy.php
|
|||
castopod/
|
||||
castopod-*.zip
|
||||
castopod-*.tar.gz
|
||||
|
||||
# Plugins
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
||||
writable/plugins.json
|
||||
writable/plugins-lock.json
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
image: code.castopod.org:5050/adaures/castopod:ci-php8.1
|
||||
image: code.castopod.org:5050/adaures/castopod:ci-php8.5
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
|
|
@ -94,14 +94,14 @@ lint-js:
|
|||
tests:
|
||||
stage: quality
|
||||
services:
|
||||
- mariadb:10.2
|
||||
- mariadb:10.11
|
||||
variables:
|
||||
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
|
||||
MYSQL_DATABASE: "test"
|
||||
MYSQL_USER: "castopod"
|
||||
MYSQL_PASSWORD: "castopod"
|
||||
script:
|
||||
- echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip-ssl
|
||||
- echo "SHOW DATABASES;" | mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip_ssl
|
||||
|
||||
# run phpunit without code coverage
|
||||
# TODO: add code coverage
|
||||
|
|
@ -135,7 +135,7 @@ bundle:
|
|||
rules:
|
||||
- if: $CI_PROJECT_NAMESPACE != "adaures"
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH =~ /^(main|beta|alpha|next)$/ || $CI_COMMIT_TAG
|
||||
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/ || $CI_COMMIT_TAG
|
||||
when: never
|
||||
- when: on_success
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
1. [First step]
|
||||
2. [Second step]
|
||||
3. [and so on...]
|
||||
3. [and so on…]
|
||||
|
||||
### Expected behavior
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ logs, and code as it's very hard to read otherwise.
|
|||
- OS: [e.g. Ubuntu server]
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Web server: [eg. Apache]
|
||||
- [any other relevant context...]
|
||||
- [any other relevant context…]
|
||||
|
||||
### Possible fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
### Is your feature request related to a problem? Please describe
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
frustrated when […]
|
||||
|
||||
### Describe the solution you'd like
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"files": ["*.md", "*.mdx"],
|
||||
"options": {
|
||||
"proseWrap": "always"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
},
|
||||
{
|
||||
"name": "next",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
+ resources/
|
||||
+ app/***
|
||||
+ modules/***
|
||||
+ plugins/***
|
||||
+ public/***
|
||||
+ themes/***
|
||||
+ vendor/***
|
||||
|
|
|
|||
|
|
@ -10,10 +10,17 @@
|
|||
"responsive",
|
||||
"variants",
|
||||
"screen",
|
||||
"layer"
|
||||
"layer",
|
||||
"config"
|
||||
]
|
||||
}
|
||||
],
|
||||
"at-rule-no-deprecated": [
|
||||
true,
|
||||
{
|
||||
"ignoreAtRules": ["apply"]
|
||||
}
|
||||
],
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
336
CHANGELOG.md
336
CHANGELOG.md
|
|
@ -1,234 +1,107 @@
|
|||
## [1.15.5](https://code.castopod.org/adaures/castopod/compare/v1.15.4...v1.15.5) (2026-02-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **docker:** add arch-specific supercronic and s6-overlay services
|
||||
([243ce3a](https://code.castopod.org/adaures/castopod/commit/243ce3a45c740352841bf67fe1ff63151f276eb9)),
|
||||
closes [#580](https://code.castopod.org/adaures/castopod/issues/580)
|
||||
|
||||
## [1.15.4](https://code.castopod.org/adaures/castopod/compare/v1.15.3...v1.15.4) (2026-02-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **emails:** display verification link in clear text for email clients only
|
||||
displaying text
|
||||
([37c89df](https://code.castopod.org/adaures/castopod/commit/37c89df3424e4f67fb05c09094560df7edbd76d4)),
|
||||
closes [#328](https://code.castopod.org/adaures/castopod/issues/328)
|
||||
|
||||
## [1.15.3](https://code.castopod.org/adaures/castopod/compare/v1.15.2...v1.15.3) (2026-02-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bundle:** include resources folder for icons during rsync
|
||||
([70ca0c7](https://code.castopod.org/adaures/castopod/commit/70ca0c7928cabb99a07de01cb4e12f5d81689bbe)),
|
||||
closes [#588](https://code.castopod.org/adaures/castopod/issues/588)
|
||||
|
||||
## [1.15.2](https://code.castopod.org/adaures/castopod/compare/v1.15.1...v1.15.2) (2026-02-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **player:** load icons locally instead of relying on vimejs picking them from
|
||||
third party scripts
|
||||
([0961987](https://code.castopod.org/adaures/castopod/commit/096198727627b3dba9c5bd28f20e90cff680316b)),
|
||||
closes [#551](https://code.castopod.org/adaures/castopod/issues/551)
|
||||
|
||||
## [1.15.1](https://code.castopod.org/adaures/castopod/compare/v1.15.0...v1.15.1) (2026-02-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **docker:** create optimized builder with docker-container driver for arm64
|
||||
builds
|
||||
([89ae2b8](https://code.castopod.org/adaures/castopod/commit/89ae2b89fd20fa31851a31fe44f514294fbcd688)),
|
||||
closes [#580](https://code.castopod.org/adaures/castopod/issues/580)
|
||||
|
||||
## [1.15.0](https://code.castopod.org/adaures/castopod/compare/v1.14.1...v1.15.0) (2026-02-16)
|
||||
|
||||
### Features
|
||||
|
||||
- **docker:** replace all-in-one image with FrankenPHP and Caddy based image +
|
||||
discard other images
|
||||
([14089f0](https://code.castopod.org/adaures/castopod/commit/14089f0542ccdf187bd64bea8ad2787b9e8c7d59))
|
||||
|
||||
## <small>1.14.1 (2026-01-31)</small>
|
||||
|
||||
- fix(i18n): set english as first item in supported locales in case locale
|
||||
negotiation does not work
|
||||
([44fb904](https://code.castopod.org/adaures/castopod/commit/44fb904))
|
||||
|
||||
## 1.14.0 (2026-01-23)
|
||||
|
||||
- feat: add Lithuanian and Czech languages
|
||||
([9582f2a](https://code.castopod.org/adaures/castopod/commit/9582f2a))
|
||||
|
||||
## <small>1.13.8 (2025-12-20)</small>
|
||||
|
||||
- chore: update CI4 to v4.6.4 + php and js packages to latest
|
||||
([133e308](https://code.castopod.org/adaures/castopod/commit/133e308))
|
||||
- fix(fediverse): match episode posts replies fields with comments in union
|
||||
query ([d438190](https://code.castopod.org/adaures/castopod/commit/d438190)),
|
||||
closes [#577](https://code.castopod.org/adaures/castopod/issues/577)
|
||||
|
||||
## <small>1.13.7 (2025-11-03)</small>
|
||||
|
||||
- fix(rss): set person's avatar url to "federation" for width and height of
|
||||
400px ([a50b0f3](https://code.castopod.org/adaures/castopod/commit/a50b0f3))
|
||||
|
||||
## <small>1.13.6 (2025-11-03)</small>
|
||||
|
||||
- fix(fediverse): access to URI in 'object' instead of going down with '->id' in
|
||||
delete case
|
||||
([41211a1](https://code.castopod.org/adaures/castopod/commit/41211a1))
|
||||
- build: update docker images' versions + docs to latest
|
||||
([07e3a9c](https://code.castopod.org/adaures/castopod/commit/07e3a9c))
|
||||
|
||||
## <small>1.13.5 (2025-08-25)</small>
|
||||
|
||||
- chore: add discourse social network
|
||||
([08a3779](https://code.castopod.org/adaures/castopod/commit/08a3779))
|
||||
- chore: fix rector issues with filters' methods return types
|
||||
([3d7969d](https://code.castopod.org/adaures/castopod/commit/3d7969d))
|
||||
- chore: update .releaserc to include more detailed release notes
|
||||
([5b4403e](https://code.castopod.org/adaures/castopod/commit/5b4403e))
|
||||
- chore: update CI to v4.6.3 + all php and js dependencies
|
||||
([842c4e4](https://code.castopod.org/adaures/castopod/commit/842c4e4))
|
||||
- fix(episodes): set dropdown menu for seasons / years to a maximum height with
|
||||
auto scroll
|
||||
([f88abd2](https://code.castopod.org/adaures/castopod/commit/f88abd2))
|
||||
- fix(fediverse): add is_private field to posts to flag private posts and hide
|
||||
them from public views
|
||||
([d5ef2ab](https://code.castopod.org/adaures/castopod/commit/d5ef2ab))
|
||||
|
||||
## [1.13.4](https://code.castopod.org/adaures/castopod/compare/v1.13.3...v1.13.4) (2025-02-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **platforms:** add podcast id when deleting platforms on save
|
||||
([019fbaf](https://code.castopod.org/adaures/castopod/commit/019fbaf74ddd7427c3b7dfaef0d2e4409aab0e7c))
|
||||
- return method instead of throwing a 404 when submitting a post
|
||||
([44ad651](https://code.castopod.org/adaures/castopod/commit/44ad65117635b6292b4653bca7e22acecb025146)),
|
||||
closes [#550](https://code.castopod.org/adaures/castopod/issues/550)
|
||||
|
||||
## [1.13.3](https://code.castopod.org/adaures/castopod/compare/v1.13.2...v1.13.3) (2025-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove exit function from podcast:import command to allow for
|
||||
episodes:compute-downloads to run
|
||||
([3359abf](https://code.castopod.org/adaures/castopod/commit/3359abf3fc7d6ddf9db2cacc3e25f7c6d99e33a6))
|
||||
|
||||
## [1.13.2](https://code.castopod.org/adaures/castopod/compare/v1.13.1...v1.13.2) (2024-12-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add downloads_count to episodes table, computed every hour
|
||||
([5182d5d](https://code.castopod.org/adaures/castopod/commit/5182d5d67aa3c6f7906d4603efcec4b48f048991))
|
||||
|
||||
## [1.13.1](https://code.castopod.org/adaures/castopod/compare/v1.13.0...v1.13.1) (2024-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **api:** cast integers when creating episode
|
||||
([7ca501d](https://code.castopod.org/adaures/castopod/commit/7ca501dd6f426a1d50ffb1ea759f1e2cc91c1d13))
|
||||
|
||||
# [1.13.0](https://code.castopod.org/adaures/castopod/compare/v1.12.11...v1.13.0) (2024-10-25)
|
||||
## [2.0.0-next.3](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.2...v2.0.0-next.3) (2024-12-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **api:** add Episode create and publish endpoints
|
||||
([75cf78e](https://code.castopod.org/adaures/castopod/commit/75cf78e972c52528dc38be050dcb1eb1f8e626fa))
|
||||
([a90cdfd](https://code.castopod.org/adaures/castopod/commit/a90cdfdcdbde7a8fb520c6815d7b757947aea055))
|
||||
- **image:** add image size's width and height
|
||||
([f50098e](https://code.castopod.org/adaures/castopod/commit/f50098ec8926c8ae40718f5f128b6de7fe721b46))
|
||||
- **plugins:** add defaultValue for all field types
|
||||
([d3a98db](https://code.castopod.org/adaures/castopod/commit/d3a98db6d0112b5f59daddd2708c09dd2e595332))
|
||||
- **plugins:** add group field type + multiple option to render field arrays
|
||||
([11ccd0e](https://code.castopod.org/adaures/castopod/commit/11ccd0ebe71d476d8c0dbfe28edcf01f7f362b83))
|
||||
- **plugins:** add html field type + CodeEditor component + rework html head
|
||||
generation
|
||||
([8cf9c6d](https://code.castopod.org/adaures/castopod/commit/8cf9c6dc833aedcccbc4cdb309b111f84d97d629))
|
||||
- **rss:** add option for 301 redirect to new feed url
|
||||
([3a7d26f](https://code.castopod.org/adaures/castopod/commit/3a7d26fdf9bfeffb9247f8efe06d9040ae2fe5ff))
|
||||
|
||||
## [1.12.11](https://code.castopod.org/adaures/castopod/compare/v1.12.10...v1.12.11) (2024-10-16)
|
||||
([8402cc2](https://code.castopod.org/adaures/castopod/commit/8402cc29d2d0c61b014a7e03e5ccce7d3c11782a))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add downloads_count to episodes table, computed every hour
|
||||
([f981937](https://code.castopod.org/adaures/castopod/commit/f9819376455c371eb5bd3c84ad938698335a3d67))
|
||||
- allow passing json to app.proxyIPs config to set it
|
||||
([7d1460b](https://code.castopod.org/adaures/castopod/commit/7d1460b8e08beb447389c604995efd931c84fd72))
|
||||
|
||||
## [1.12.10](https://code.castopod.org/adaures/castopod/compare/v1.12.9...v1.12.10) (2024-10-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- set user as www-data when running cron jobs in docker's supervisord config
|
||||
([be3b6db](https://code.castopod.org/adaures/castopod/commit/be3b6db207204e14c9ad5d4d84384b15e0dbfa84))
|
||||
|
||||
## [1.12.9](https://code.castopod.org/adaures/castopod/compare/v1.12.8...v1.12.9) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **fediverse:** add "processing" and "failed" statuses to better manage
|
||||
broadcast load
|
||||
([cf9e072](https://code.castopod.org/adaures/castopod/commit/cf9e0724fcdb8d0194676880cc3b088b221f5a38)),
|
||||
closes [#511](https://code.castopod.org/adaures/castopod/issues/511)
|
||||
|
||||
## [1.12.8](https://code.castopod.org/adaures/castopod/compare/v1.12.7...v1.12.8) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **podcast-model:** always query podcast from database when clearing cache
|
||||
([995ca5b](https://code.castopod.org/adaures/castopod/commit/995ca5b197f8f917102a108dd07d1f81e99cc8e6))
|
||||
|
||||
## [1.12.7](https://code.castopod.org/adaures/castopod/compare/v1.12.6...v1.12.7) (2024-08-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **episode:** do not change slug when editing episode title
|
||||
([89bf73b](https://code.castopod.org/adaures/castopod/commit/89bf73b869c28c2fcffa3dcbc3660fac3b6bf988)),
|
||||
closes [#513](https://code.castopod.org/adaures/castopod/issues/513)
|
||||
- **preview:** delete episode preview cache after editing episode
|
||||
([6a2cdd0](https://code.castopod.org/adaures/castopod/commit/6a2cdd066ee13efc6489901bbdcbcc5fea35cd71)),
|
||||
closes [#514](https://code.castopod.org/adaures/castopod/issues/514)
|
||||
|
||||
## [1.12.6](https://code.castopod.org/adaures/castopod/compare/v1.12.5...v1.12.6) (2024-08-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
([cbf739e](https://code.castopod.org/adaures/castopod/commit/cbf739e95cc0ad6e83a21353b8f4678e68d74f63))
|
||||
- **api:** cast integers when creating episode
|
||||
([775b302](https://code.castopod.org/adaures/castopod/commit/775b302f7c886e30e133c8a8c68764301b6c663b))
|
||||
- **docker-image:** clear cache to account for new assets and data structure
|
||||
changes
|
||||
([e41245d](https://code.castopod.org/adaures/castopod/commit/e41245d2e758bce2a404749398bef89998638561)),
|
||||
([63c763f](https://code.castopod.org/adaures/castopod/commit/63c763f941195b3758c4b91acd8c350a5e7bb9c2)),
|
||||
closes [#510](https://code.castopod.org/adaures/castopod/issues/510)
|
||||
|
||||
## [1.12.5](https://code.castopod.org/adaures/castopod/compare/v1.12.4...v1.12.5) (2024-07-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- edit remap functions to get episode in episode admin controllers
|
||||
([9f74cca](https://code.castopod.org/adaures/castopod/commit/9f74cca342fedd896977efd2e89d0143959f3c4f))
|
||||
- **episode:** do not change slug when editing episode title
|
||||
([a83afb0](https://code.castopod.org/adaures/castopod/commit/a83afb0004511db80337806577fbc36f8d777116)),
|
||||
closes [#513](https://code.castopod.org/adaures/castopod/issues/513)
|
||||
- **fediverse:** add "processing" and "failed" statuses to better manage
|
||||
broadcast load
|
||||
([1d7583d](https://code.castopod.org/adaures/castopod/commit/1d7583d738219574ae3d45d294dc94e7e406472b)),
|
||||
closes [#511](https://code.castopod.org/adaures/castopod/issues/511)
|
||||
- **icons:** set correct names for lock and lock-unlock icons in premium banner
|
||||
([37ee6d3](https://code.castopod.org/adaures/castopod/commit/37ee6d35b4bb66ce23dc271fb846200d1be0e7f6))
|
||||
- **plugins:** clear cache after activating or deactivating plugin
|
||||
([08c7df2](https://code.castopod.org/adaures/castopod/commit/08c7df2a5d5be340490c78deeef823167eb1b2fc))
|
||||
- **plugins:** delete relevant cache when submitting settings
|
||||
([00bd4c0](https://code.castopod.org/adaures/castopod/commit/00bd4c02ee23b181d74e7731626bfec3b1ff4916))
|
||||
- **podcast-model:** always query podcast from database when clearing cache
|
||||
([d30c49c](https://code.castopod.org/adaures/castopod/commit/d30c49cdff380c15db4f1851631a255a5baffcbe))
|
||||
- **premium-podcasts:** update query to validate subscription
|
||||
([2b1bbf3](https://code.castopod.org/adaures/castopod/commit/2b1bbf34303ead927f433b5c7d5d888ca3799954))
|
||||
- **preview:** delete episode preview cache after editing episode
|
||||
([732d429](https://code.castopod.org/adaures/castopod/commit/732d42923d0d7a66ff1ebd5841458e4205060560)),
|
||||
closes [#514](https://code.castopod.org/adaures/castopod/issues/514)
|
||||
- **release:** add conventional-changelog-conventionalcommits for CHANGELOG
|
||||
generation
|
||||
([6934c8a](https://code.castopod.org/adaures/castopod/commit/6934c8aa8f0b7f9eea7c3f6f4089c56b2391d9a6))
|
||||
- **rss:** add subscription id to cache name to prevent premium feeds from
|
||||
overlapping
|
||||
([5310d86](https://code.castopod.org/adaures/castopod/commit/5310d8648af6d43b9090f8d9f8066f7b3a8f0aa7))
|
||||
([74f9325](https://code.castopod.org/adaures/castopod/commit/74f9325946d03a0d4efce57045e41cc9454ff97c))
|
||||
- set user as www-data when running cron jobs in docker's supervisord config
|
||||
([65d74f1](https://code.castopod.org/adaures/castopod/commit/65d74f14e612be3757c9304518eee112705f5ff9))
|
||||
- typo in EpisodeController remap function to get episode
|
||||
([f288a75](https://code.castopod.org/adaures/castopod/commit/f288a750f580ab19b04a170cc76bf8769084e19d))
|
||||
- update select and multi-select options to value/label arrays
|
||||
([63f93f5](https://code.castopod.org/adaures/castopod/commit/63f93f585bec4a11022cc8c75deb34968cba2348))
|
||||
|
||||
## [1.12.4](https://code.castopod.org/adaures/castopod/compare/v1.12.3...v1.12.4) (2024-07-30)
|
||||
### Internal
|
||||
|
||||
### Bug Fixes
|
||||
- **plugins:** create Field objects per field type in settings forms + handle
|
||||
rendering in class
|
||||
([34be5bc](https://code.castopod.org/adaures/castopod/commit/34be5bccabb7531afdcc6ebaf1dd39e4dfbe0677))
|
||||
- remove fields from podcast and episode entities to be replaced with plugins
|
||||
([b869acb](https://code.castopod.org/adaures/castopod/commit/b869acb3a988a3616d883a41c25d9c8409bd5518))
|
||||
- rename controller methods for views and actions to be more consistent
|
||||
([85704bf](https://code.castopod.org/adaures/castopod/commit/85704bfbe03fe5e38ff5e76a0e1cf0e5f1275f57))
|
||||
- update CodeIgniter to v4.5.6
|
||||
([f295e9a](https://code.castopod.org/adaures/castopod/commit/f295e9aa4ca3129df24a22779f7c19bba7fac370))
|
||||
- update codigniter-icons to v1.0.1
|
||||
([fa6967e](https://code.castopod.org/adaures/castopod/commit/fa6967e65cef1705b19cbb205132c4c751507d53))
|
||||
- update js dependencies to latest
|
||||
([70c9797](https://code.castopod.org/adaures/castopod/commit/70c97971fcf5bbeee826578057ae0e3afbbbd8a8))
|
||||
|
||||
- **icons:** set correct names for lock and lock-unlock icons in premium banner
|
||||
([94deaab](https://code.castopod.org/adaures/castopod/commit/94deaab3cd0912ff1a585bee174a096a84c68384))
|
||||
- **premium-podcasts:** update query to validate subscription
|
||||
([0e6d294](https://code.castopod.org/adaures/castopod/commit/0e6d2945f215453abbe7d9f90afd012d2507846b))
|
||||
|
||||
## [1.12.3](https://code.castopod.org/adaures/castopod/compare/v1.12.2...v1.12.3) (2024-07-04)
|
||||
# [2.0.0-next.2](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.1...v2.0.0-next.2) (2024-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **audio-player:** set player icons to default instead of missing Castopod's
|
||||
([c89d298](https://code.castopod.org/adaures/castopod/commit/c89d29867e122fe7d4d5563f0ab1e9993e2ece16))
|
||||
|
||||
## [1.12.2](https://code.castopod.org/adaures/castopod/compare/v1.12.1...v1.12.2) (2024-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
([0ba0a25](https://code.castopod.org/adaures/castopod/commit/0ba0a25b11bd67aeeb47a8179b72152dfd4a36da))
|
||||
- broken icon call in frontend default pages template
|
||||
([d8d2eb9](https://code.castopod.org/adaures/castopod/commit/d8d2eb92b741ecfc956b416db481f8c2dee84864))
|
||||
|
||||
## [1.12.1](https://code.castopod.org/adaures/castopod/compare/v1.12.0...v1.12.1) (2024-07-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
([3228362](https://code.castopod.org/adaures/castopod/commit/322836254e86be7878e21438177ee8f73f03a2fa))
|
||||
- **manifest:** set repository url as required in docstring typings
|
||||
([a8c81b3](https://code.castopod.org/adaures/castopod/commit/a8c81b3fa19a28dbd608027c231dcac31eafb38f))
|
||||
- set correct icons parameters in map and funding links views
|
||||
([b129813](https://code.castopod.org/adaures/castopod/commit/b129813ea5d38436563639b51ec9ed2882644228)),
|
||||
([5d35524](https://code.castopod.org/adaures/castopod/commit/5d355248753be24e3cf324144ff076f2fc23be88)),
|
||||
closes [#500](https://code.castopod.org/adaures/castopod/issues/500)
|
||||
|
||||
# [1.12.0](https://code.castopod.org/adaures/castopod/compare/v1.11.0...v1.12.0) (6/28/2024)
|
||||
### Features
|
||||
|
||||
- **plugins:** add `minCastopodVersion` to denote incompatibility with previous
|
||||
Castopod versions
|
||||
([fc9ea75](https://code.castopod.org/adaures/castopod/commit/fc9ea7597e454e5c7c7af043d29af7bbe119e342))
|
||||
- **plugins:** load and display LICENSE.md file if found in plugin's directory
|
||||
([fee7905](https://code.castopod.org/adaures/castopod/commit/fee7905935a9adf963b4485b437fe4d972c14b5f))
|
||||
|
||||
# [2.0.0-next.1](https://code.castopod.org/adaures/castopod/compare/v1.11.0...v2.0.0-next.1) (6/19/2024)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
@ -248,14 +121,65 @@
|
|||
([fc4f982](https://code.castopod.org/adaures/castopod/commit/fc4f9825568cd4384c5b3cfe972accd146548807)),
|
||||
closes [#473](https://code.castopod.org/adaures/castopod/issues/473)
|
||||
|
||||
### Build System
|
||||
|
||||
- release next major version as prerelease
|
||||
([8275226](https://code.castopod.org/adaures/castopod/commit/827522643e9f8a5ea9be05b4847dc637f0f43a13))
|
||||
|
||||
### Features
|
||||
|
||||
- add Plugins module with base files for plugins architecture
|
||||
([7253e13](https://code.castopod.org/adaures/castopod/commit/7253e13ac2118f6f165f54ea0cbcd63d51ab9205))
|
||||
- **plugins:** abstract settings form for general, podcast and episode types
|
||||
([b62b483](https://code.castopod.org/adaures/castopod/commit/b62b483ad9ff114a22a9ee52e1a1a2c9fa444d42))
|
||||
- **plugins:** activate / deactivate plugin using settings table
|
||||
([27d2a1b](https://code.castopod.org/adaures/castopod/commit/27d2a1b0ffba9454dd54cbb4251a2d179b09762a))
|
||||
- **plugins:** add aside with plugin metadata next to plugin's readme
|
||||
([dfb7888](https://code.castopod.org/adaures/castopod/commit/dfb7888aeb689b4066abc37084e08cd7f1d0f15d))
|
||||
- **plugins:** add before channel/item hooks to allow podcast/episode data edit
|
||||
when generating rss
|
||||
([80d2c48](https://code.castopod.org/adaures/castopod/commit/80d2c48ee265cb32ed0d710c488292fcbc120044))
|
||||
- **plugins:** add json schema definition for plugin manifest
|
||||
([b5eddf3](https://code.castopod.org/adaures/castopod/commit/b5eddf351f6f6fa1c299fbac31cbd056ef232330))
|
||||
- **plugins:** add methods to easily retrieve general, podcast and episode
|
||||
settings in hooks methods
|
||||
([3a900bb](https://code.castopod.org/adaures/castopod/commit/3a900bbab68b819cedf8943540d2ee0aeb6e8539))
|
||||
- **plugins:** add new field types + validate & cast user data before storing
|
||||
settings
|
||||
([6f833fc](https://code.castopod.org/adaures/castopod/commit/6f833fc76a3aa6c6b87c27ad18a2fb90e537e21e))
|
||||
- **plugins:** add options to manifest for building forms and storing plugin
|
||||
settings
|
||||
([3d8aedf](https://code.castopod.org/adaures/castopod/commit/3d8aedf9c34e6927b6d3b11445d5f0e669b347d7))
|
||||
- **plugins:** add settings page for podcast and episode if defined in the
|
||||
plugin's manifest
|
||||
([89ac92f](https://code.castopod.org/adaures/castopod/commit/89ac92fb412a04231ce52fd6480c9ab893b19ef5))
|
||||
- **plugins:** add siteHead hook to add custom meta tags to public pages
|
||||
([e80a33b](https://code.castopod.org/adaures/castopod/commit/e80a33bf2ad4fe1b47037add7470a6c2770f4036))
|
||||
- **plugins:** display errors when plugin is invalid instead of crashing
|
||||
([8ec7909](https://code.castopod.org/adaures/castopod/commit/8ec79097bbdbcbce622518ef61c068f20e0ef74e))
|
||||
- **plugins:** handle empty states and long strings in UI
|
||||
([45ac2a4](https://code.castopod.org/adaures/castopod/commit/45ac2a4be96532b9456e6af1d26ba4ada3649303))
|
||||
- **plugins:** load and validate plugin manifest.json
|
||||
([1510e36](https://code.castopod.org/adaures/castopod/commit/1510e36c0acd2b254622ec230acd1d2461ee9bf3))
|
||||
- **plugins:** load plugins using file locator service
|
||||
([587938d](https://code.castopod.org/adaures/castopod/commit/587938d2bf307b823af143586b9ec9e9b44e8dc1))
|
||||
- **plugins:** load README.md file to view plugin's instructions in UI
|
||||
([e6bfdfc](https://code.castopod.org/adaures/castopod/commit/e6bfdfc3902705285701c13c8067fe0f538425c6))
|
||||
- **plugins:** register plugins using Plugin.php file instead of namespace +
|
||||
simplify i18n structure
|
||||
([2035c39](https://code.castopod.org/adaures/castopod/commit/2035c39fd138a1fd408516bd1972ab6a02544c10))
|
||||
- **plugins:** uninstall plugins via CLI and admin UI
|
||||
([9a80de4](https://code.castopod.org/adaures/castopod/commit/9a80de40686bbf4288da21cc2a6dde8036580e47))
|
||||
- set owner email to hidden by default in podcast create form
|
||||
([7a6d9df](https://code.castopod.org/adaures/castopod/commit/7a6d9df6db8a6184b8250ced0475f3e741dde7f4))
|
||||
- support podcast:txt tag with verify use case
|
||||
([57e459e](https://code.castopod.org/adaures/castopod/commit/57e459e187ed048430f4137172e22396cd02bf81)),
|
||||
closes [#468](https://code.castopod.org/adaures/castopod/issues/468)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- next major release including plugins architecture
|
||||
|
||||
# [1.11.0](https://code.castopod.org/adaures/castopod/compare/v1.10.5...v1.11.0) (4/17/2024)
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
|
|
@ -1,128 +1,162 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
# Contributor Covenant 3.0 Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
We pledge to make our community welcoming, safe, and equitable for all.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
We are committed to fostering an environment that respects and promotes the
|
||||
dignity, rights, and contributions of all individuals, regardless of
|
||||
characteristics including race, ethnicity, caste, color, age, physical
|
||||
characteristics, neurodiversity, disability, sex or gender, gender identity or
|
||||
expression, sexual orientation, language, philosophy or religion, national or
|
||||
social origin, socio-economic position, level of education, or other status. The
|
||||
same privileges of participation are extended to everyone who participates in
|
||||
good faith and in accordance with this Covenant.
|
||||
|
||||
## Our Standards
|
||||
## Encouraged Behaviors
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
While acknowledging differences in social norms, we all strive to meet our
|
||||
community's expectations for positive behavior. We also understand that our
|
||||
words and actions may be interpreted differently than we intend based on
|
||||
culture, background, or native language.
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
With these considerations in mind, we agree to behave mindfully toward each
|
||||
other and act in ways that center our shared values, including:
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
1. Respecting the **purpose of our community**, our activities, and our ways of
|
||||
gathering.
|
||||
2. Engaging **kindly and honestly** with others.
|
||||
3. Respecting **different viewpoints** and experiences.
|
||||
4. **Taking responsibility** for our actions and contributions.
|
||||
5. Gracefully giving and accepting **constructive feedback**.
|
||||
6. Committing to **repairing harm** when it occurs.
|
||||
7. Behaving in other ways that promote and sustain the **well-being of our
|
||||
community**.
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
## Restricted Behaviors
|
||||
|
||||
## Enforcement Responsibilities
|
||||
We agree to restrict the following behaviors in our community. Instances,
|
||||
threats, and promotion of these behaviors are violations of this Code of
|
||||
Conduct.
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
1. **Harassment.** Violating explicitly expressed boundaries or engaging in
|
||||
unnecessary personal attention after any clear request to stop.
|
||||
2. **Character attacks.** Making insulting, demeaning, or pejorative comments
|
||||
directed at a community member or group of people.
|
||||
3. **Stereotyping or discrimination.** Characterizing anyone’s personality or
|
||||
behavior on the basis of immutable identities or traits.
|
||||
4. **Sexualization.** Behaving in a way that would generally be considered
|
||||
inappropriately intimate in the context or purpose of the community.
|
||||
5. **Violating confidentiality**. Sharing or acting on someone's personal or
|
||||
private information without their permission.
|
||||
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm
|
||||
toward any person or group.
|
||||
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
### Other Restrictions
|
||||
|
||||
1. **Misleading identity.** Impersonating someone else for any reason, or
|
||||
pretending to be someone else to evade enforcement actions.
|
||||
2. **Failing to credit sources.** Not properly crediting the sources of content
|
||||
you contribute.
|
||||
3. **Promotional materials**. Sharing marketing or other commercial content in a
|
||||
way that is outside the norms of the community.
|
||||
4. **Irresponsible communication.** Failing to responsibly present content which
|
||||
includes, links or describes any other restricted behaviors.
|
||||
|
||||
## Reporting an Issue
|
||||
|
||||
Tensions can occur between community members even when they are trying their
|
||||
best to collaborate. Not every conflict represents a code of conduct violation,
|
||||
and this Code of Conduct reinforces encouraged behaviors and norms that can help
|
||||
avoid conflicts and minimize harm.
|
||||
|
||||
When an incident does occur, it is important to report it promptly. To report a
|
||||
possible violation, email us at [abuse@castopod.org](mailto:abuse@castopod.org).
|
||||
|
||||
Community Moderators take reports of violations seriously and will make every
|
||||
effort to respond in a timely manner. They will investigate all reports of code
|
||||
of conduct violations, reviewing messages, logs, and recordings, or interviewing
|
||||
witnesses and other participants. Community Moderators will keep investigation
|
||||
and enforcement actions as transparent as possible while prioritizing safety and
|
||||
confidentiality. In order to honor these values, enforcement actions are carried
|
||||
out in private with the involved parties, but communicating to the whole
|
||||
community may be part of a mutually agreed upon resolution.
|
||||
|
||||
## Addressing and Repairing Harm
|
||||
|
||||
If an investigation by the Community Moderators finds that this Code of Conduct
|
||||
has been violated, the following enforcement ladder may be used to determine how
|
||||
best to repair harm, based on the incident's impact on the individuals involved
|
||||
and the community as a whole. Depending on the severity of a violation, lower
|
||||
rungs on the ladder may be skipped.
|
||||
|
||||
1. Warning
|
||||
1. Event: A violation involving a single incident or series of incidents.
|
||||
2. Consequence: A private, written warning from the Community Moderators.
|
||||
3. Repair: Examples of repair include a private written apology,
|
||||
acknowledgement of responsibility, and seeking clarification on
|
||||
expectations.
|
||||
2. Temporarily Limited Activities
|
||||
1. Event: A repeated incidence of a violation that previously resulted in a
|
||||
warning, or the first incidence of a more serious violation.
|
||||
2. Consequence: A private, written warning with a time-limited cooldown
|
||||
period designed to underscore the seriousness of the situation and give
|
||||
the community members involved time to process the incident. The cooldown
|
||||
period may be limited to particular communication channels or interactions
|
||||
with particular community members.
|
||||
3. Repair: Examples of repair may include making an apology, using the
|
||||
cooldown period to reflect on actions and impact, and being thoughtful
|
||||
about re-entering community spaces after the period is over.
|
||||
3. Temporary Suspension
|
||||
1. Event: A pattern of repeated violation which the Community Moderators have
|
||||
tried to address with warnings, or a single serious violation.
|
||||
2. Consequence: A private written warning with conditions for return from
|
||||
suspension. In general, temporary suspensions give the person being
|
||||
suspended time to reflect upon their behavior and possible corrective
|
||||
actions.
|
||||
3. Repair: Examples of repair include respecting the spirit of the
|
||||
suspension, meeting the specified conditions for return, and being
|
||||
thoughtful about how to reintegrate with the community when the suspension
|
||||
is lifted.
|
||||
4. Permanent Ban
|
||||
1. Event: A pattern of repeated code of conduct violations that other steps
|
||||
on the ladder have failed to resolve, or a violation so serious that the
|
||||
Community Moderators determine there is no way to keep the community safe
|
||||
with this person as a member.
|
||||
2. Consequence: Access to all community spaces, tools, and communication
|
||||
channels is removed. In general, permanent bans should be rarely used,
|
||||
should have strong reasoning behind them, and should only be resorted to
|
||||
if working through other remedies has failed to change the behavior.
|
||||
3. Repair: There is no possible repair in cases of this severity.
|
||||
|
||||
This enforcement ladder is intended as a guideline. It does not limit the
|
||||
ability of Community Managers to use their discretion and judgment, in keeping
|
||||
with the best interests of our community.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
an individual is officially representing the community in public or other
|
||||
spaces. Examples of representing our community include using an official email
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[abuse@castopod.org](mailto:abuse@castopod.org). All complaints will be reviewed
|
||||
and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version 3.0,
|
||||
permanently available at
|
||||
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
Contributor Covenant is stewarded by the Organization for Ethical Source and
|
||||
licensed under CC BY-SA 4.0. To view a copy of this license, visit
|
||||
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
For answers to common questions about Contributor Covenant, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
|
||||
Translations are provided at
|
||||
[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
|
||||
Additional enforcement and community guideline resources can be found at
|
||||
[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources).
|
||||
The enforcement ladder was inspired by the work of
|
||||
[Mozilla’s code of conduct team](https://github.com/mozilla/inclusion).
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Castopod is a web app based on the `php` framework
|
||||
[CodeIgniter 4](https://codeigniter.com).
|
||||
|
||||
We use [Docker](https://www.docker.com/) quickly setup a dev environment. A
|
||||
We use [Docker](https://www.docker.com/) to quickly setup a dev environment. A
|
||||
`docker-compose.yml` and `Dockerfile` are included in the project's root folder
|
||||
to help you kickstart your contribution.
|
||||
|
||||
|
|
@ -16,9 +16,9 @@ to help you kickstart your contribution.
|
|||
|
||||
### 1. Pre-requisites
|
||||
|
||||
0. Install [docker](https://docs.docker.com/get-docker).
|
||||
0. Install [Docker](https://docs.docker.com/get-docker).
|
||||
|
||||
1. Clone Castopod project by running:
|
||||
1. Clone the Castopod repository by running:
|
||||
|
||||
```bash
|
||||
git clone https://code.castopod.org/adaures/castopod.git
|
||||
|
|
@ -79,7 +79,7 @@ to help you kickstart your contribution.
|
|||
> [CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html)
|
||||
> for more info.
|
||||
|
||||
3. (for docker desktop) Add the repository you've cloned to docker desktop's
|
||||
3. (for Docker desktop) Add the repository you've cloned to Docker desktop's
|
||||
`Settings` > `Resources` > `File Sharing`
|
||||
|
||||
### 2. (recommended) Develop inside the app container with VSCode
|
||||
|
|
@ -96,7 +96,7 @@ required services will be loaded automagically! 🪄
|
|||
> The VSCode window will reload inside the dev container. Expect several
|
||||
> minutes during first load as it is building all necessary services.
|
||||
|
||||
**Note**: The dev container will start by running Castopod's php server.
|
||||
**Note**: The dev container will start by running Castopod's PHP server.
|
||||
During development, you will have to start [Vite](https://vitejs.dev)'s dev
|
||||
server for compiling the typescript code and styles:
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ required services will be loaded automagically! 🪄
|
|||
pnpm run dev
|
||||
```
|
||||
|
||||
If there is any issue with the php server not running, you can restart them
|
||||
If there is any issue with the PHP server not running, you can restart them
|
||||
using the following commands:
|
||||
|
||||
```bash
|
||||
|
|
@ -157,9 +157,9 @@ To see your changes, go to:
|
|||
|
||||
You do not wish to use the VSCode devcontainer? No problem!
|
||||
|
||||
1. Start docker containers manually:
|
||||
1. Start the Docker containers manually:
|
||||
|
||||
Go to project's root folder and run:
|
||||
Go to the project's root folder and run:
|
||||
|
||||
```bash
|
||||
# starts all services declared in docker-compose.yml file
|
||||
|
|
@ -223,14 +223,14 @@ You do not wish to use the VSCode devcontainer? No problem!
|
|||
> For more info, check out the
|
||||
> [Composer documentation](https://getcomposer.org/doc/).
|
||||
|
||||
2. Install javascript dependencies with [pnpm](https://pnpm.io/)
|
||||
2. Install JavaScript dependencies with [pnpm](https://pnpm.io/)
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The javascript dependencies aren't included in the repository. Pnpm will
|
||||
> The JavaScript dependencies aren't included in the repository. Pnpm will
|
||||
> check the `package.json` and `pnpm-lock.yaml` files to download the
|
||||
> packages with the right versions. The dependencies will live under the
|
||||
> `node_module` folder. For more info, check out the
|
||||
|
|
@ -249,7 +249,7 @@ You do not wish to use the VSCode devcontainer? No problem!
|
|||
|
||||
> [!NOTE]
|
||||
> The static assets generated live under the `public/assets` folder, it
|
||||
> includes javascript, styles, images, fonts, icons and svg files.
|
||||
> includes JavaScript, styles, images, fonts, icons and svg files.
|
||||
|
||||
### Initialize and populate database
|
||||
|
||||
|
|
@ -291,7 +291,7 @@ You do not wish to use the VSCode devcontainer? No problem!
|
|||
php spark db:seed DevSuperadminSeeder
|
||||
```
|
||||
|
||||
3. (optionnal) Populate the database with test data:
|
||||
3. (optional) Populate the database with test data:
|
||||
- Populate with fake podcast analytics:
|
||||
|
||||
```bash
|
||||
|
|
@ -312,13 +312,13 @@ You do not wish to use the VSCode devcontainer? No problem!
|
|||
docker-compose logs --tail 50 --follow --timestamps app
|
||||
```
|
||||
|
||||
- Interact with redis server using included redis-cli command:
|
||||
- Interact with the Redis server using included redis-cli command:
|
||||
|
||||
```bash
|
||||
docker exec -it castopod_redis redis-cli
|
||||
```
|
||||
|
||||
- Monitor the redis container:
|
||||
- Monitor the Redis container:
|
||||
|
||||
```bash
|
||||
docker-compose logs --tail 50 --follow --timestamps redis
|
||||
|
|
@ -354,10 +354,37 @@ docker-compose down
|
|||
docker-compose build app
|
||||
```
|
||||
|
||||
Check [docker](https://docs.docker.com/engine/reference/commandline/docker/) and
|
||||
Check [Docker](https://docs.docker.com/engine/reference/commandline/docker/) and
|
||||
[docker-compose](https://docs.docker.com/compose/reference/) documentations for
|
||||
more insights.
|
||||
|
||||
### Updating Documentation
|
||||
|
||||
Castopod's documentation is written in Markdown and uses the Astro Starlight
|
||||
framework. To update Castopod's documentation, including the Getting Started
|
||||
guide and User Guide:
|
||||
|
||||
1. Change directories to the `docs` directory and install the dependencies:
|
||||
|
||||
```bash
|
||||
cd docs/
|
||||
pnpm i
|
||||
```
|
||||
|
||||
2. Start the documentation development server:
|
||||
|
||||
```bash
|
||||
pnpm run dev --host
|
||||
```
|
||||
|
||||
3. The documentation development server runs on port 4321. In your browser visit
|
||||
`http://localhost:4321/docs`. If the page displays a 404 Not Found error,
|
||||
click on the Castopod logo in the upper left hand corner of the page and the
|
||||
documentation should load.
|
||||
|
||||
4. Edit the Markdown files with your documentation updates. The Astro Starlight
|
||||
development server will automatically update each time you save a change.
|
||||
|
||||
## Known issues
|
||||
|
||||
### Allocation failed - JavaScript heap out of memory
|
||||
|
|
@ -396,7 +423,7 @@ You may use Linux user namespaces to fix this on your machine:
|
|||
username:100000:65536
|
||||
```
|
||||
|
||||
3. Restart docker:
|
||||
3. Restart Docker:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ accurate comments, etc.) and any other requirements (such as test coverage).
|
|||
Adhering to the following process is the best way to get your work included in
|
||||
the project:
|
||||
|
||||
1. [Fork](https://docs.gitlab.com/ee/gitlab-basics/fork-project.html) the
|
||||
project, clone your fork, and configure the remotes:
|
||||
1. [Fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html)
|
||||
the project, clone your fork, and configure the remotes:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ Javascript dependencies can be found in the [package.json](./package.json) file.
|
|||
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
|
||||
- [RemixIcon](https://remixicon.com/)
|
||||
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
|
||||
- [OPAWG/User agent list](https://github.com/opawg/user-agents)
|
||||
- [OPAWG/User agent list](https://github.com/opawg/user-agents-v2)
|
||||
([by Open Podcast Analytics Working Group](https://github.com/opawg))
|
||||
([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
|
||||
([MIT license](https://github.com/opawg/user-agents-v2/blob/master/LICENSE))
|
||||
- [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents)
|
||||
([by Open Podcast Analytics Working Group](https://github.com/opawg))
|
||||
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -1,7 +1,7 @@
|
|||
<div align="center">
|
||||
<h1>
|
||||
<a href="https://castopod.org/">
|
||||
<img src="https://docs.castopod.org/images/castopod-logo-inline.svg" alt="Castopod" height="64px" />
|
||||
<img src="./docs/src/assets/castopod-logo-inline.svg" alt="Castopod" height="64px" />
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
|
|
@ -46,18 +46,17 @@ Thanks goes to these wonderful people
|
|||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
<table class="all-contributors-table">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yassinedoghri"><img src="https://code.castopod.org/uploads/-/system/user/avatar/3/avatar.png?s=100" width="100px;" alt="Yassine Doghri"/><br /><sub><b>Yassine Doghri</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=yassinedoghri" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=yassinedoghri" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-yassinedoghri" title="Maintenance">🚧</a> <a href="#content-yassinedoghri" title="Content">🖋</a> <a href="#design-yassinedoghri" title="Design">🎨</a> <a href="#a11y-yassinedoghri" title="Accessibility">️️️️♿️</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-yassinedoghri" title="Answering Questions">💬</a> <a href="#mentoring-yassinedoghri" title="Mentoring">🧑🏫</a> <a href="#infra-yassinedoghri" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-yassinedoghri" title="Ideas, Planning, & Feedback">🤔</a> <a href="#projectManagement-yassinedoghri" title="Project Management">📆</a> <a href="https://blog.castopod.org/author/yassinedoghri/" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://yassinedoghri.com"><img src="https://avatars.githubusercontent.com/u/11021441?v=4?s=100" width="100px;" alt="Yassine Doghri"/><br /><sub><b>Yassine Doghri</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=yassinedoghri" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=yassinedoghri" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-yassinedoghri" title="Maintenance">🚧</a> <a href="#content-yassinedoghri" title="Content">🖋</a> <a href="#design-yassinedoghri" title="Design">🎨</a> <a href="#a11y-yassinedoghri" title="Accessibility">️️️️♿️</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-yassinedoghri" title="Answering Questions">💬</a> <a href="#mentoring-yassinedoghri" title="Mentoring">🧑🏫</a> <a href="#infra-yassinedoghri" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-yassinedoghri" title="Ideas, Planning, & Feedback">🤔</a> <a href="#projectManagement-yassinedoghri" title="Project Management">📆</a> <a href="https://blog.castopod.org/author/yassinedoghri/" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/benjamin"><img src="https://code.castopod.org/uploads/-/system/user/avatar/2/avatar.png?s=100" width="100px;" alt="Benjamin Bellamy"/><br /><sub><b>Benjamin Bellamy</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=benjamin" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=benjamin" title="Reviewed Pull Requests">👀</a> <a href="#content-benjamin" title="Content">🖋</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-benjamin" title="Answering Questions">💬</a> <a href="#infra-benjamin" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-benjamin" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://blog.castopod.org/author/benjamin-bellamy/" title="Blogposts">📝</a> <a href="#projectManagement-benjamin" title="Project Management">📆</a> <a href="#talk-benjamin" title="Talks">📢</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ola-hn"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ola Hneini"/><br /><sub><b>Ola Hneini</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=ola" title="Reviewed Pull Requests">👀</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#maintenance-ola" title="Maintenance">🚧</a> <a href="#question-ola" title="Answering Questions">💬</a> <a href="#ideas-ola" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mamot.fr/@rdelaage"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Romain de Laage"/><br /><sub><b>Romain de Laage</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="#infra-rdelaage" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-rdelaage" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/lyonelbernard"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Lyonel Bernard"/><br /><sub><b>Lyonel Bernard</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Lyonel" title="Bug reports">🐛</a> <a href="#question-Lyonel" title="Answering Questions">💬</a> <a href="#audio-Lyonel" title="Audio">🔊</a> <a href="#ideas-Lyonel" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.crypticchameleon.com/"><img src="https://secure.gravatar.com/avatar/7c2a721b52d0763673a600e8f01bd745?s=80&d=identicon?s=100" width="100px;" alt="Christopher Lagonick-Weitzel"/><br /><sub><b>Christopher Lagonick-Weitzel</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ctlw83" title="Bug reports">🐛</a> <a href="#question-ctlw83" title="Answering Questions">💬</a> <a href="#audio-ctlw83" title="Audio">🔊</a> <a href="#ideas-ctlw83" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ernestoacosta.me/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ernesto Acosta"/><br /><sub><b>Ernesto Acosta</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ernestoacostame" title="Bug reports">🐛</a> <a href="#audio-ernestoacostame" title="Audio">🔊</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-ernestoacostame" title="Answering Questions">💬</a> <a href="#ideas-ernestoacostame" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fedi.bzh/@ewen"><img src="https://mastodon.fedi.bzh/system/accounts/avatars/000/000/002/original/6f387690a504ae46.jpg?s=100" width="100px;" alt="Ewen"/><br /><sub><b>Ewen</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-3wen" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Behel"><img src="https://secure.gravatar.com/avatar/ad63ee8ef8e3db8253d21e5012d2724f?s=80&d=identicon?s=100" width="100px;" alt="Bastien Luneteau"/><br /><sub><b>Bastien Luneteau</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=Behel" title="Bug reports">🐛</a></td>
|
||||
|
|
@ -66,7 +65,7 @@ Thanks goes to these wonderful people
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/mspanc"><img src="https://secure.gravatar.com/avatar/eed8337939641eac5ad0b570bd6acf96?s=80&d=identicon?s=100" width="100px;" alt="Marcin Lewandowski"/><br /><sub><b>Marcin Lewandowski</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=mspanc" title="Bug reports">🐛</a> <a href="#ideas-mspanc" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/SJanik"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sebastian Janik"/><br /><sub><b>Sebastian Janik</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/patryk"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Patryk Karczmarczyk"/><br /><sub><b>Patryk Karczmarczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/ddenis"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="denis d"/><br /><sub><b>denis d</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ddenis" title="Bug reports">🐛</a> <a href="#ideas-ddenis" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/douglaskastle"><img src="https://secure.gravatar.com/avatar/b7e652ba4b6bcd440afa069e7f7bc9e6?s=80&d=identicon?s=100" width="100px;" alt="Douglas Kastle"/><br /><sub><b>Douglas Kastle</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=douglaskastle" title="Bug reports">🐛</a> <a href="#ideas-douglaskastle" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
|
|
@ -75,7 +74,7 @@ Thanks goes to these wonderful people
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/jonas"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Jonas S"/><br /><sub><b>Jonas S</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/yannL"><img src="https://secure.gravatar.com/avatar/9c46600ce566ec6d526370d8e104b1c8?s=80&d=identicon?s=100" width="100px;" alt="LEFEBVRE Yann"/><br /><sub><b>LEFEBVRE Yann</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=yannL" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/spaetz"><img src="https://secure.gravatar.com/avatar/278e1af65e82993efd0ba7bbbacf6435?s=80&d=identicon?s=100" width="100px;" alt="Sebastian Späth"/><br /><sub><b>Sebastian Späth</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=spaetz" title="Bug reports">🐛</a> <a href="#ideas-spaetz" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/rocky"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="rocky III"/><br /><sub><b>rocky III</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=rocky" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Regenpfeifer"><img src="https://code.castopod.org/uploads/-/system/user/avatar/103/avatar.png?s=100" width="100px;" alt="Hermann Josef Eckl"/><br /><sub><b>Hermann Josef Eckl</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Regenpfeifer" title="Bug reports">🐛</a></td>
|
||||
|
|
@ -84,7 +83,7 @@ Thanks goes to these wonderful people
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://achouvardas.eu/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Angelos Chouvardas"/><br /><sub><b>Angelos Chouvardas</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fjerland.no/@eivind"><img src="https://mastodon.fjerland.no/system/accounts/avatars/107/769/768/295/192/222/original/e5c985fea6487dcb.jpg?s=100" width="100px;" alt="Eivind"/><br /><sub><b>Eivind</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/forght"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15073833/large/82d1e2e443a6df7edc43a7405dfeeb75_default.png?s=100" width="100px;" alt="forght"/><br /><sub><b>forght</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/glottis0q"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15209934/large/8b17ef6a7399f0b82a8198f87c224195.png?s=100" width="100px;" alt="glottis0q"/><br /><sub><b>glottis0q</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mstdn.fr/@ButterflyOfFire"><img src="https://static.mstdn.fr/static/accounts/avatars/000/065/901/original/5908e93ad5447f15.png?s=100" width="100px;" alt="ButterflyOfFire"/><br /><sub><b>ButterflyOfFire</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
|
|
@ -93,7 +92,7 @@ Thanks goes to these wonderful people
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/cthtc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15211502/large/ed0651060cb8474a9519b5168bd377c1_default.png?s=100" width="100px;" alt="CTHTC"/><br /><sub><b>CTHTC</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/retrograde"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15021651/large/b10c4057f85bf4de49c7fdf01354ecde.jpeg?s=100" width="100px;" alt="Russian Retro"/><br /><sub><b>Russian Retro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/mareklach"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13572324/large/3eeba8d569c247ace33862bf4ef4748f.jpeg?s=100" width="100px;" alt="Marek L'ach"/><br /><sub><b>Marek L'ach</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gunchleoc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13043878/large/3223f7b606296a8b1c92c5de39c459a2_default.png?s=100" width="100px;" alt="GunChleoc"/><br /><sub><b>GunChleoc</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gabisnow"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15214858/large/5b083bdf9c9e9de67cc6ee72a6c8db18_default.png?s=100" width="100px;" alt="GabiSnow"/><br /><sub><b>GabiSnow</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
|
|
@ -102,25 +101,24 @@ Thanks goes to these wonderful people
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://dimitriregnier.net/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Dimitri Regnier"/><br /><sub><b>Dimitri Regnier</b></sub></a><br /><a href="#ideas-dimregnier" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://im.irithys.com/@thy"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15405614/large/3086461c47cce0a0c031925e5f943412.png?s=100" width="100px;" alt="irithys"/><br /><sub><b>irithys</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/caos30"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sergi"/><br /><sub><b>Sergi</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
</tr><br />
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/xosem"><img src="https://crowdin-static.downloads.crowdin.com/avatar/12617257/large/a201650da44fed28890b0e0d8477a663.jpg?s=100" width="100px;" alt="ghose (XoseM)"/><br /><sub><b>ghose (XoseM)</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/basen1982"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Andreas Olsson"/><br /><sub><b>Andreas Olsson</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/leonfrom"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="leonfrom"/><br /><sub><b>leonfrom</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/agentcobra57"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="agentcobra"/><br /><sub><b>agentcobra</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/alephoto85"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15094649/large/530391f54157af52ae33058ec15b0f99.jpg?s=100" width="100px;" alt="Alessandro"/><br /><sub><b>Alessandro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/liimee"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="liimee"/><br /><sub><b>liimee</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahmedsabouni"><img src="https://avatars.githubusercontent.com/u/74497842?v=4?s=100" width="100px;" alt="Ahmed Sabouni"/><br /><sub><b>Ahmed Sabouni</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
|
||||
</tr><br />
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KrzysztofDomanczyk"><img src="https://avatars.githubusercontent.com/u/75178474?v=4?s=100" width="100px;" alt="KrzysztofDomanczyk"/><br /><sub><b>KrzysztofDomanczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/NeoluxConsulting"><img src="https://secure.gravatar.com/avatar/6e745565356330c1e29a85d52bffdaa1?s=80&d=identicon?s=100" width="100px;" alt="Guy Martin"/><br /><sub><b>Guy Martin</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=NeoluxConsulting" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Dwev"><img src="https://avatars.githubusercontent.com/u/46626050?v=4?s=100" width="100px;" alt="Guy Martin"/><br /><sub><b>Guy Martin</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Dwev" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prcutler"><img src="https://avatars.githubusercontent.com/u/67276?v=4?s=100" width="100px;" alt="Paul Cutler"/><br /><sub><b>Paul Cutler</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#question-prcutler" title="Answering Questions">💬</a> <a href="#ideas-prcutler" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nateritter"><img src="https://avatars.githubusercontent.com/u/198798?v=4?s=100" width="100px;" alt="Nate Ritter"/><br /><sub><b>Nate Ritter</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
|
|
@ -157,10 +155,10 @@ backers. If you'd like to help, please consider
|
|||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://docs.castopod.org/images/sponsors/adaures.svg" target="_blank" rel="noopener noreferrer"><img height="48" src="https://docs.castopod.org/images/sponsors/adaures.svg" alt="Netlify" /></a>
|
||||
<a href="https://docs.castopod.org/images/sponsors/adaures.svg" target="_blank" rel="noopener noreferrer"><img height="48" src="./docs/src/assets/images/sponsors/adaures.svg" alt="Ad Aures" /></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://nlnet.nl/project/Castopod/" target="_blank" rel="noopener noreferrer"><img src="https://docs.castopod.org/images/sponsors/nlnet.svg" alt="NLnet Logo" height="48" /></a>
|
||||
<a href="https://nlnet.nl/project/Castopod/" target="_blank" rel="noopener noreferrer"><img src="./docs/src/assets/images/sponsors/nlnet.svg" alt="NLnet Logo" height="48" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use Override;
|
||||
|
||||
class App extends BaseConfig
|
||||
{
|
||||
|
|
@ -123,19 +124,17 @@ class App extends BaseConfig
|
|||
* @var list<string>
|
||||
*/
|
||||
public array $supportedLocales = [
|
||||
'en', // keep english language first in case locale negotiation does not work
|
||||
'en',
|
||||
'fr',
|
||||
'br',
|
||||
'ca',
|
||||
'cs',
|
||||
'de',
|
||||
'es',
|
||||
'lt',
|
||||
'nn-no',
|
||||
'pl',
|
||||
'de',
|
||||
'pt-br',
|
||||
'sr-latn',
|
||||
'nn-no',
|
||||
'es',
|
||||
'zh-hans',
|
||||
'ca',
|
||||
'br',
|
||||
'sr-latn',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -274,6 +273,7 @@ class App extends BaseConfig
|
|||
*
|
||||
* @param mixed $property
|
||||
*/
|
||||
#[Override]
|
||||
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix): void
|
||||
{
|
||||
// if attempting to set property from ENV, first set to empty string
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Autoload extends AutoloadConfig
|
|||
'Modules\Media' => ROOTPATH . 'modules/Media/',
|
||||
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
|
||||
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
|
||||
'Modules\Plugins' => ROOTPATH . 'modules/Plugins/',
|
||||
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
|
||||
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
|
||||
'Modules\Update' => ROOTPATH . 'modules/Update/',
|
||||
|
|
@ -55,7 +56,6 @@ class Autoload extends AutoloadConfig
|
|||
'Themes' => ROOTPATH . 'themes',
|
||||
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
|
||||
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
|
||||
'Vite' => APPPATH . 'Libraries/Vite/',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -93,7 +93,7 @@ class Autoload extends AutoloadConfig
|
|||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
|
||||
public $files = [];
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
|
|
@ -106,5 +106,5 @@ class Autoload extends AutoloadConfig
|
|||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public $helpers = ['auth', 'setting'];
|
||||
public $helpers = ['auth', 'setting', 'plugins'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,19 @@ use CodeIgniter\Config\BaseConfig;
|
|||
|
||||
class CURLRequest extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CURLRequest Share Connection Options
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Share connection options between requests.
|
||||
*
|
||||
* @var list<int>
|
||||
*
|
||||
* @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
|
||||
*/
|
||||
public array $shareConnectionOptions = [CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_DNS];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CURLRequest Share Options
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Cache\Handlers\ApcuHandler;
|
||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||
|
|
@ -76,6 +77,7 @@ class Cache extends BaseConfig
|
|||
* --------------------------------------------------------------------------
|
||||
* File settings
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Your file storage preferences can be specified below, if you are using
|
||||
* the File driver.
|
||||
*
|
||||
|
|
@ -90,6 +92,7 @@ class Cache extends BaseConfig
|
|||
* -------------------------------------------------------------------------
|
||||
* Memcached settings
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Your Memcached servers can be specified below, if you are using
|
||||
* the Memcached drivers.
|
||||
*
|
||||
|
|
@ -111,14 +114,24 @@ class Cache extends BaseConfig
|
|||
* Your Redis server can be specified below, if you are using
|
||||
* the Redis or Predis drivers.
|
||||
*
|
||||
* @var array{host?: string, password?: string|null, port?: int, timeout?: int, database?: int}
|
||||
* @var array{
|
||||
* host?: string,
|
||||
* password?: string|null,
|
||||
* port?: int,
|
||||
* timeout?: int,
|
||||
* async?: bool,
|
||||
* persistent?: bool,
|
||||
* database?: int
|
||||
* }
|
||||
*/
|
||||
public array $redis = [
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'database' => 0,
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'async' => false, // specific to Predis and ignored by the native Redis extension
|
||||
'persistent' => false,
|
||||
'database' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -132,6 +145,7 @@ class Cache extends BaseConfig
|
|||
* @var array<string, class-string<CacheInterface>>
|
||||
*/
|
||||
public array $validHandlers = [
|
||||
'apcu' => ApcuHandler::class,
|
||||
'dummy' => DummyHandler::class,
|
||||
'file' => FileHandler::class,
|
||||
'memcached' => MemcachedHandler::class,
|
||||
|
|
@ -158,4 +172,28 @@ class Cache extends BaseConfig
|
|||
* @var bool|list<string>
|
||||
*/
|
||||
public $cacheQueryString = false;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Web Page Caching: Cache Status Codes
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* HTTP status codes that are allowed to be cached. Only responses with
|
||||
* these status codes will be cached by the PageCache filter.
|
||||
*
|
||||
* Default: [] - Cache all status codes (backward compatible)
|
||||
*
|
||||
* Recommended: [200] - Only cache successful responses
|
||||
*
|
||||
* You can also use status codes like:
|
||||
* [200, 404, 410] - Cache successful responses and specific error codes
|
||||
* [200, 201, 202, 203, 204] - All 2xx successful responses
|
||||
*
|
||||
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
|
||||
* Consider restricting to [200] for production applications to avoid
|
||||
* caching errors that should be temporary.
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
public array $cacheStatusCodes = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ declare(strict_types=1);
|
|||
|
|
||||
| NOTE: this constant is updated upon release with Continuous Integration.
|
||||
*/
|
||||
defined('CP_VERSION') || define('CP_VERSION', '1.15.5');
|
||||
defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3');
|
||||
|
||||
/*
|
||||
| --------------------------------------------------------------------
|
||||
|
|
@ -24,10 +24,23 @@ defined('CP_VERSION') || define('CP_VERSION', '1.15.5');
|
|||
| classes should use.
|
||||
|
|
||||
| NOTE: changing this will require manually modifying the
|
||||
| existing namespaces of App\* namespaced-classes.
|
||||
| existing namespaces of App* namespaced-classes.
|
||||
*/
|
||||
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
|
||||
|
||||
/*
|
||||
| --------------------------------------------------------------------
|
||||
| Plugins Path
|
||||
| --------------------------------------------------------------------
|
||||
|
|
||||
| This defines the folder in which plugins will live.
|
||||
*/
|
||||
defined('PLUGINS_PATH') ||
|
||||
define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR);
|
||||
|
||||
defined('PLUGINS_KEY_PATTERN') ||
|
||||
define('PLUGINS_KEY_PATTERN', '[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*');
|
||||
|
||||
/*
|
||||
| --------------------------------------------------------------------------
|
||||
| Composer Path
|
||||
|
|
|
|||
|
|
@ -26,14 +26,24 @@ class ContentSecurityPolicy extends BaseConfig
|
|||
*/
|
||||
public ?string $reportURI = null;
|
||||
|
||||
/**
|
||||
* Specifies a reporting endpoint to which violation reports ought to be sent.
|
||||
*/
|
||||
public ?string $reportTo = null;
|
||||
|
||||
/**
|
||||
* Instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large
|
||||
* numbers of old URLs that need to be rewritten.
|
||||
*/
|
||||
public bool $upgradeInsecureRequests = false;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CSP DIRECTIVES SETTINGS
|
||||
// NOTE: once you set a policy to 'none', it cannot be further restricted
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Will default to self if not overridden
|
||||
* Will default to `'self'` if not overridden
|
||||
*
|
||||
* @var list<string>|string|null
|
||||
*/
|
||||
|
|
@ -46,6 +56,21 @@ class ContentSecurityPolicy extends BaseConfig
|
|||
*/
|
||||
public string | array $scriptSrc = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for JavaScript <script> elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $scriptSrcElem = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for JavaScript inline event
|
||||
* handlers and JavaScript URLs.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $scriptSrcAttr = 'self';
|
||||
|
||||
/**
|
||||
* Lists allowed stylesheets' URLs.
|
||||
*
|
||||
|
|
@ -53,6 +78,21 @@ class ContentSecurityPolicy extends BaseConfig
|
|||
*/
|
||||
public string | array $styleSrc = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for stylesheets <link> elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $styleSrcElem = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for stylesheets inline
|
||||
* style attributes and `<style>` elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $styleSrcAttr = 'self';
|
||||
|
||||
/**
|
||||
* Defines the origins from which images can be loaded.
|
||||
*
|
||||
|
|
@ -132,6 +172,11 @@ class ContentSecurityPolicy extends BaseConfig
|
|||
*/
|
||||
public string | array | null $manifestSrc = null;
|
||||
|
||||
/**
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $workerSrc = [];
|
||||
|
||||
/**
|
||||
* Limits the kinds of plugins a page may invoke.
|
||||
*
|
||||
|
|
@ -147,17 +192,17 @@ class ContentSecurityPolicy extends BaseConfig
|
|||
public string | array | null $sandbox = null;
|
||||
|
||||
/**
|
||||
* Nonce tag for style
|
||||
* Nonce placeholder for style tags.
|
||||
*/
|
||||
public string $styleNonceTag = '{csp-style-nonce}';
|
||||
|
||||
/**
|
||||
* Nonce tag for script
|
||||
* Nonce placeholder for script tags.
|
||||
*/
|
||||
public string $scriptNonceTag = '{csp-script-nonce}';
|
||||
|
||||
/**
|
||||
* Replace nonce tag automatically
|
||||
* Replace nonce tag automatically?
|
||||
*/
|
||||
public bool $autoNonce = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ class Email extends BaseConfig
|
|||
*/
|
||||
public string $SMTPHost = '';
|
||||
|
||||
/**
|
||||
* Which SMTP authentication method to use: login, plain
|
||||
*/
|
||||
public string $SMTPAuthMethod = 'login';
|
||||
|
||||
/**
|
||||
* SMTP Username
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -25,6 +25,23 @@ class Encryption extends BaseConfig
|
|||
*/
|
||||
public string $key = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Previous Encryption Keys
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* When rotating encryption keys, add old keys here to maintain ability
|
||||
* to decrypt data encrypted with previous keys. Encryption always uses
|
||||
* the current $key. Decryption tries current key first, then falls back
|
||||
* to previous keys if decryption fails.
|
||||
*
|
||||
* In .env file, use comma-separated string:
|
||||
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $previousKeys = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Encryption Driver to Use
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ Events::on('pre_system', static function (): void {
|
|||
// Hot Reload route - for framework use on the hot reloader.
|
||||
if (ENVIRONMENT === 'development') {
|
||||
service('routes')->get('__hot-reload', static function (): void {
|
||||
(new HotReloader())->run();
|
||||
new HotReloader()
|
||||
->run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class Fediverse extends FediverseBaseConfig
|
|||
}
|
||||
|
||||
['dirname' => $dirname, 'extension' => $extension, 'filename' => $filename] = pathinfo(
|
||||
$defaultBanner['path']
|
||||
$defaultBanner['path'],
|
||||
);
|
||||
$defaultBannerPath = $filename;
|
||||
if ($dirname !== '.') {
|
||||
|
|
|
|||
|
|
@ -63,4 +63,13 @@ class Format extends BaseConfig
|
|||
'application/xml' => 0,
|
||||
'text/xml' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Maximum depth for JSON encoding.
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This value determines how deep the JSON encoder will traverse nested structures.
|
||||
*/
|
||||
public int $jsonEncodeDepth = 512;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Generators extends BaseConfig
|
|||
*
|
||||
* YOU HAVE BEEN WARNED!
|
||||
*
|
||||
* @var array<string, array<string, string>|string>
|
||||
* @var array<string, string|array<string,string>>
|
||||
*/
|
||||
public array $views = [
|
||||
'make:cell' => [
|
||||
|
|
|
|||
42
app/Config/Hostnames.php
Normal file
42
app/Config/Hostnames.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Config;
|
||||
|
||||
class Hostnames
|
||||
{
|
||||
// List of known two-part TLDs for subdomain extraction
|
||||
public const TWO_PART_TLDS = [
|
||||
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
|
||||
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
|
||||
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
|
||||
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
|
||||
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
|
||||
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
|
||||
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
|
||||
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
|
||||
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
|
||||
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
|
||||
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
|
||||
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
|
||||
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
|
||||
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
|
||||
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
|
||||
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
|
||||
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
|
||||
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
|
||||
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
|
||||
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
|
||||
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
|
||||
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
|
||||
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
|
||||
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
|
||||
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
|
||||
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
|
||||
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
|
||||
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
|
||||
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
|
||||
];
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ class Images extends BaseConfig
|
|||
|
||||
/**
|
||||
* The path to the image library. Required for ImageMagick, GraphicsMagick, or NetPBM.
|
||||
*
|
||||
* @deprecated 4.7.0 No longer used.
|
||||
*/
|
||||
public string $libraryPath = '/usr/local/bin/convert';
|
||||
|
||||
|
|
|
|||
|
|
@ -46,4 +46,19 @@ class Migrations extends BaseConfig
|
|||
* - Y_m_d_His_
|
||||
*/
|
||||
public string $timestampFormat = 'Y-m-d-His_';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Enable/Disable Migration Lock
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Locking is disabled by default.
|
||||
*
|
||||
* When enabled, it will prevent multiple migration processes
|
||||
* from running at the same time by using a lock mechanism.
|
||||
*
|
||||
* This is useful in production environments to avoid conflicts
|
||||
* or race conditions during concurrent deployments.
|
||||
*/
|
||||
public bool $lock = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,13 +279,8 @@ class Mimes
|
|||
'srt' => ['application/x-subrip', 'text/srt', 'text/plain', 'application/octet-stream'],
|
||||
'vtt' => ['text/vtt', 'text/plain'],
|
||||
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
|
||||
'stl' => [
|
||||
'application/sla',
|
||||
'application/vnd.ms-pki.stl',
|
||||
'application/x-navistyle',
|
||||
'model/stl',
|
||||
'application/octet-stream',
|
||||
],
|
||||
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle', 'model/stl',
|
||||
'application/octet-stream', ],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -312,7 +307,7 @@ class Mimes
|
|||
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
|
||||
* @return string|null The extension determined, or null if unable to match.
|
||||
*/
|
||||
public static function guessExtensionFromType(string $type, string $proposedExtension = null): ?string
|
||||
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null): ?string
|
||||
{
|
||||
$type = trim(strtolower($type), '. ');
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ namespace Config;
|
|||
*
|
||||
* NOTE: This class does not extend BaseConfig for performance reasons.
|
||||
* So you cannot replace the property values with Environment Variables.
|
||||
*
|
||||
* WARNING: Do not use these options when running the app in the Worker Mode.
|
||||
*/
|
||||
class Optimize
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ namespace Config;
|
|||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*/
|
||||
|
||||
class Paths
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* Placeholder definitions
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
/** @var RouteCollection $routes */
|
||||
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
|
||||
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
|
||||
|
|
@ -154,7 +159,7 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
|||
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
|
||||
'as' => 'episode-comment-replies',
|
||||
]);
|
||||
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::attemptLike/$1/$2/$3', [
|
||||
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::likeAction/$1/$2/$3', [
|
||||
'as' => 'episode-comment-attempt-like',
|
||||
]);
|
||||
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
|
||||
|
|
@ -222,9 +227,9 @@ $routes->get('/pages/(:slug)', 'PageController::index/$1', [
|
|||
* Overwriting Fediverse routes file
|
||||
*/
|
||||
$routes->group('@(:podcastHandle)', static function ($routes): void {
|
||||
$routes->post('posts/new', 'PostController::attemptCreate/$1', [
|
||||
$routes->post('posts/new', 'PostController::createAction/$1', [
|
||||
'as' => 'post-attempt-create',
|
||||
'filter' => 'permission:podcast#.manage-publications',
|
||||
'filter' => 'permission:podcast$1.manage-publications',
|
||||
]);
|
||||
// Post
|
||||
$routes->group('posts/(:uuid)', static function ($routes): void {
|
||||
|
|
@ -259,13 +264,13 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
|||
'filter' => 'allow-cors',
|
||||
]);
|
||||
// Actions
|
||||
$routes->post('action', 'PostController::attemptAction/$1/$2', [
|
||||
$routes->post('action', 'PostController::action/$1/$2', [
|
||||
'as' => 'post-attempt-action',
|
||||
'filter' => 'permission:podcast#.interact-as',
|
||||
'filter' => 'permission:podcast$1.interact-as',
|
||||
]);
|
||||
$routes->post(
|
||||
'block-actor',
|
||||
'PostController::attemptBlockActor/$1/$2',
|
||||
'PostController::blockActorAction/$1/$2',
|
||||
[
|
||||
'as' => 'post-attempt-block-actor',
|
||||
'filter' => 'permission:fediverse.manage-blocks',
|
||||
|
|
@ -273,25 +278,25 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
|||
);
|
||||
$routes->post(
|
||||
'block-domain',
|
||||
'PostController::attemptBlockDomain/$1/$2',
|
||||
'PostController::blockDomainAction/$1/$2',
|
||||
[
|
||||
'as' => 'post-attempt-block-domain',
|
||||
'filter' => 'permission:fediverse.manage-blocks',
|
||||
],
|
||||
);
|
||||
$routes->post('delete', 'PostController::attemptDelete/$1/$2', [
|
||||
$routes->post('delete', 'PostController::deleteAction/$1/$2', [
|
||||
'as' => 'post-attempt-delete',
|
||||
'filter' => 'permission:podcast#.manage-publications',
|
||||
'filter' => 'permission:podcast$1.manage-publications',
|
||||
]);
|
||||
$routes->get(
|
||||
'remote/(:postAction)',
|
||||
'PostController::remoteAction/$1/$2/$3',
|
||||
'PostController::remoteActionAction/$1/$2/$3',
|
||||
[
|
||||
'as' => 'post-remote-action',
|
||||
],
|
||||
);
|
||||
});
|
||||
$routes->get('follow', 'ActorController::follow/$1', [
|
||||
$routes->get('follow', 'ActorController::followView/$1', [
|
||||
'as' => 'follow',
|
||||
]);
|
||||
$routes->get('outbox', 'ActorController::outbox/$1', [
|
||||
|
|
|
|||
|
|
@ -98,6 +98,15 @@ class Routing extends BaseRouting
|
|||
*/
|
||||
public bool $autoRoute = false;
|
||||
|
||||
/**
|
||||
* If TRUE, the system will look for attributes on controller
|
||||
* class and methods that can run before and after the
|
||||
* controller/method.
|
||||
*
|
||||
* If FALSE, will ignore any attributes.
|
||||
*/
|
||||
public bool $useControllerAttributes = true;
|
||||
|
||||
/**
|
||||
* For Defined Routes.
|
||||
* If TRUE, will enable the use of the 'prioritize' option
|
||||
|
|
@ -136,7 +145,7 @@ class Routing extends BaseRouting
|
|||
*
|
||||
* If you enable this, $translateURIDashes is ignored.
|
||||
*
|
||||
* Default: true
|
||||
* Default: false
|
||||
*/
|
||||
public bool $translateUriToCamelCase = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Config;
|
||||
|
||||
use App\Libraries\Breadcrumb;
|
||||
use App\Libraries\HtmlHead;
|
||||
use App\Libraries\Negotiate;
|
||||
use App\Libraries\Router;
|
||||
use CodeIgniter\Config\BaseService;
|
||||
|
|
@ -33,7 +34,7 @@ class Services extends BaseService
|
|||
public static function router(
|
||||
?RouteCollectionInterface $routes = null,
|
||||
?Request $request = null,
|
||||
bool $getShared = true
|
||||
bool $getShared = true,
|
||||
): CodeIgniterRouter {
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('router', $routes, $request);
|
||||
|
|
@ -51,7 +52,7 @@ class Services extends BaseService
|
|||
*/
|
||||
public static function negotiator(
|
||||
?RequestInterface $request = null,
|
||||
bool $getShared = true
|
||||
bool $getShared = true,
|
||||
): CodeIgniterHTTPNegotiate {
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('negotiator', $request);
|
||||
|
|
@ -70,4 +71,13 @@ class Services extends BaseService
|
|||
|
||||
return new Breadcrumb();
|
||||
}
|
||||
|
||||
public static function html_head(bool $getShared = true): HtmlHead
|
||||
{
|
||||
if ($getShared) {
|
||||
return self::getSharedInstance('html_head');
|
||||
}
|
||||
|
||||
return new HtmlHead();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class Session extends BaseConfig
|
|||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The session storage driver to use:
|
||||
* - `CodeIgniter\Session\Handlers\ArrayHandler` (for testing)
|
||||
* - `CodeIgniter\Session\Handlers\FileHandler`
|
||||
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
||||
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Tasks\Config\Tasks as BaseTasks;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Tasks\Scheduler;
|
||||
|
||||
class Tasks extends BaseTasks
|
||||
class Tasks extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
|
@ -17,7 +17,7 @@ class Tasks extends BaseTasks
|
|||
* If true, will log the time it takes for each task to run.
|
||||
* Requires the settings table to have been created previously.
|
||||
*/
|
||||
public bool $logPerformance = true;
|
||||
public bool $logPerformance = false;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -117,4 +117,29 @@ class Toolbar extends BaseConfig
|
|||
* @var list<string>
|
||||
*/
|
||||
public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env'];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Ignored HTTP Headers
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
|
||||
* HTML response. This is correct for full page loads, but it breaks requests
|
||||
* that expect only a clean HTML fragment.
|
||||
*
|
||||
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
|
||||
* manage navigation on the client side. Injecting the Debug Toolbar into their
|
||||
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
|
||||
* (such as infinite loops or "Maximum call stack size exceeded").
|
||||
*
|
||||
* Any request containing one of the following headers is treated as a
|
||||
* client-managed or partial request, and the Debug Toolbar injection is skipped.
|
||||
*
|
||||
* @var array<string, string|null>
|
||||
*/
|
||||
public array $disableOnHeaders = [
|
||||
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
|
||||
'HX-Request' => 'true', // HTMX requests
|
||||
'X-Up-Version' => null, // Unpoly partial requests
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,9 +232,13 @@ class UserAgents extends BaseConfig
|
|||
*/
|
||||
public array $robots = [
|
||||
'googlebot' => 'Googlebot',
|
||||
'google-pagerenderer' => 'Google Page Renderer',
|
||||
'google-read-aloud' => 'Google Read Aloud',
|
||||
'google-safety' => 'Google Safety Bot',
|
||||
'msnbot' => 'MSNBot',
|
||||
'baiduspider' => 'Baiduspider',
|
||||
'bingbot' => 'Bing',
|
||||
'bingpreview' => 'BingPreview',
|
||||
'slurp' => 'Inktomi Slurp',
|
||||
'yahoo' => 'Yahoo',
|
||||
'ask jeeves' => 'Ask Jeeves',
|
||||
|
|
@ -250,5 +254,11 @@ class UserAgents extends BaseConfig
|
|||
'ia_archiver' => 'Alexa Crawler',
|
||||
'MJ12bot' => 'Majestic-12',
|
||||
'Uptimebot' => 'Uptimebot',
|
||||
'duckduckbot' => 'DuckDuckBot',
|
||||
'sogou' => 'Sogou Spider',
|
||||
'exabot' => 'Exabot',
|
||||
'bot' => 'Generic Bot',
|
||||
'crawler' => 'Generic Crawler',
|
||||
'spider' => 'Generic Spider',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Config;
|
||||
|
||||
use App\Validation\FileRules as AppFileRules;
|
||||
use App\Validation\OtherRules;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Validation\StrictRules\CreditCardRules;
|
||||
use CodeIgniter\Validation\StrictRules\FileRules;
|
||||
|
|
@ -24,6 +25,7 @@ class Validation extends BaseConfig
|
|||
FileRules::class,
|
||||
CreditCardRules::class,
|
||||
AppFileRules::class,
|
||||
OtherRules::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -54,4 +54,21 @@ class View extends BaseView
|
|||
* @var list<class-string<ViewDecoratorInterface>>
|
||||
*/
|
||||
public array $decorators = [Decorator::class];
|
||||
|
||||
/**
|
||||
* Subdirectory within app/Views for namespaced view overrides.
|
||||
*
|
||||
* Namespaced views will be searched in:
|
||||
*
|
||||
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
|
||||
*
|
||||
* This allows application-level overrides for package or module views
|
||||
* without modifying vendor source files.
|
||||
*
|
||||
* Examples:
|
||||
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
|
||||
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
|
||||
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
|
||||
*/
|
||||
public string $appOverridesFolder = 'overrides';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,42 @@
|
|||
<?php
|
||||
|
||||
// app/Config/Vite.php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniterVite\Config\Vite as CodeIgniterViteConfig;
|
||||
use CodeIgniterVite\Config\Vite as ViteConfig;
|
||||
|
||||
class Vite extends CodeIgniterViteConfig
|
||||
class Vite extends ViteConfig
|
||||
{
|
||||
public string $environment = 'production';
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
public string $serverOrigin = 'http://localhost:5173';
|
||||
$adminGateway = config('Admin')
|
||||
->gateway;
|
||||
$installGateway = config('Install')
|
||||
->gateway;
|
||||
|
||||
public string $resourcesDir = 'resources';
|
||||
|
||||
public string $assetsDir = 'assets';
|
||||
|
||||
public string $manifest = '.vite/manifest.json';
|
||||
|
||||
public string $manifestCacheName = 'vite-manifest';
|
||||
|
||||
/**
|
||||
* @var array<array{routes:list<string>,exclude?:list<string>,assets:list<string>}>
|
||||
*/
|
||||
public array $routesAssets = [];
|
||||
$this->routesAssets = [
|
||||
[
|
||||
'routes' => ['*'],
|
||||
'exclude' => [$adminGateway . '*', $installGateway . '*'],
|
||||
'assets' => ['styles/site.css', 'js/app.ts', 'js/podcast.ts', 'js/audio-player.ts'],
|
||||
],
|
||||
[
|
||||
'routes' => ['/map'],
|
||||
'assets' => ['js/map.ts'],
|
||||
],
|
||||
[
|
||||
'routes' => ['/' . $adminGateway . '*'],
|
||||
'assets' => ['styles/admin.css', 'js/admin.ts', 'js/admin-audio-player.ts'],
|
||||
],
|
||||
[
|
||||
'routes' => [$installGateway . '*'],
|
||||
'assets' => ['styles/install.css'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
app/Config/WorkerMode.php
Normal file
52
app/Config/WorkerMode.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* This configuration controls how CodeIgniter behaves when running
|
||||
* in worker mode (with FrankenPHP).
|
||||
*/
|
||||
class WorkerMode
|
||||
{
|
||||
/**
|
||||
* Persistent Services
|
||||
*
|
||||
* List of service names that should persist across requests.
|
||||
* These services will NOT be reset between requests.
|
||||
*
|
||||
* Services not in this list will be reset for each request to prevent
|
||||
* state leakage.
|
||||
*
|
||||
* Recommended persistent services:
|
||||
* - `autoloader`: PSR-4 autoloading configuration
|
||||
* - `locator`: File locator
|
||||
* - `exceptions`: Exception handler
|
||||
* - `commands`: CLI commands registry
|
||||
* - `codeigniter`: Main application instance
|
||||
* - `superglobals`: Superglobals wrapper
|
||||
* - `routes`: Router configuration
|
||||
* - `cache`: Cache instance
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public array $persistentServices = [
|
||||
'autoloader',
|
||||
'locator',
|
||||
'exceptions',
|
||||
'commands',
|
||||
'codeigniter',
|
||||
'superglobals',
|
||||
'routes',
|
||||
'cache',
|
||||
];
|
||||
|
||||
/**
|
||||
* Force Garbage Collection
|
||||
*
|
||||
* Whether to force garbage collection after each request.
|
||||
* Helps prevent memory leaks at a small performance cost.
|
||||
*/
|
||||
public bool $forceGarbageCollection = true;
|
||||
}
|
||||
|
|
@ -22,19 +22,16 @@ class ActorController extends FediverseActorController
|
|||
*/
|
||||
protected $helpers = ['svg', 'components', 'misc', 'seo'];
|
||||
|
||||
public function follow(): string
|
||||
public function followView(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->registerPodcastWebpageHit($this->actor->podcast->id);
|
||||
}
|
||||
// @phpstan-ignore-next-line
|
||||
$this->registerPodcastWebpageHit($this->actor->podcast->id);
|
||||
|
||||
helper(['form', 'components', 'svg']);
|
||||
// @phpstan-ignore-next-line
|
||||
set_follow_metatags($this->actor);
|
||||
$data = [
|
||||
// @phpstan-ignore-next-line
|
||||
'metatags' => get_follow_metatags($this->actor),
|
||||
'actor' => $this->actor,
|
||||
'actor' => $this->actor,
|
||||
];
|
||||
|
||||
return view('podcast/follow', $data);
|
||||
|
|
|
|||
|
|
@ -5,35 +5,25 @@ declare(strict_types=1);
|
|||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Override;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ViewThemes\Theme;
|
||||
|
||||
/**
|
||||
* BaseController provides a convenient place for loading components and performing functions that are needed by all
|
||||
* your controllers. Extend this class in any new controllers: class Home extends BaseController
|
||||
* your controllers.
|
||||
*
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
* Extend this class in any new controllers:
|
||||
* ```
|
||||
* class Home extends BaseController
|
||||
* ```
|
||||
*
|
||||
* For security, be sure to declare any new methods as protected or private.
|
||||
*/
|
||||
abstract class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Instance of the main Request object.
|
||||
*
|
||||
* @var IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Instance of the main response object.
|
||||
*
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* An array of helpers to be loaded automatically upon
|
||||
* class instantiation. These helpers will be available
|
||||
|
|
@ -49,21 +39,19 @@ abstract class BaseController extends Controller
|
|||
*/
|
||||
// protected $session;
|
||||
|
||||
#[Override]
|
||||
public function initController(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
LoggerInterface $logger
|
||||
LoggerInterface $logger,
|
||||
): void {
|
||||
// Load here all helpers you want to be available in your controllers that extend BaseController.
|
||||
// Caution: Do not put the this below the parent::initController() call below.
|
||||
$this->helpers = [...$this->helpers, 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
|
||||
|
||||
// Caution: Do not edit this line.
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
||||
// Preload any models, libraries, etc, here.
|
||||
// $this->session = service('session');
|
||||
|
||||
Theme::setTheme('app');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,11 @@ declare(strict_types=1);
|
|||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class ColorsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Instance of the main response object.
|
||||
*
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
public function index(): Response
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
$cacheName = 'colors.css';
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ class CreditsController extends BaseController
|
|||
'content_markdown' => '',
|
||||
]);
|
||||
|
||||
$allPodcasts = (new PodcastModel())->findAll();
|
||||
$allCredits = (new CreditModel())->findAll();
|
||||
$allPodcasts = new PodcastModel()
|
||||
->findAll();
|
||||
$allCredits = new CreditModel()
|
||||
->findAll();
|
||||
|
||||
// Unlike the carpenter, we make a tree from a table:
|
||||
$personGroup = null;
|
||||
|
|
@ -164,10 +166,10 @@ class CreditsController extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
set_page_metatags($page);
|
||||
$data = [
|
||||
'metatags' => get_page_metatags($page),
|
||||
'page' => $page,
|
||||
'credits' => $credits,
|
||||
'page' => $page,
|
||||
'credits' => $credits,
|
||||
];
|
||||
|
||||
$found = view('pages/credits', $data);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ use App\Models\EpisodeModel;
|
|||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
|
@ -24,17 +23,11 @@ use CodeIgniter\HTTP\URI;
|
|||
use Modules\Analytics\Config\Analytics;
|
||||
use Modules\PremiumPodcasts\Entities\Subscription;
|
||||
use Modules\PremiumPodcasts\Models\SubscriptionModel;
|
||||
use Override;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class EpisodeAudioController extends Controller
|
||||
{
|
||||
/**
|
||||
* Instance of the main Request object.
|
||||
*
|
||||
* @var IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
|
||||
* other controllers that extend Analytics.
|
||||
|
|
@ -49,10 +42,11 @@ class EpisodeAudioController extends Controller
|
|||
|
||||
protected Analytics $analyticsConfig;
|
||||
|
||||
#[Override]
|
||||
public function initController(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
LoggerInterface $logger
|
||||
LoggerInterface $logger,
|
||||
): void {
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
|
@ -71,7 +65,7 @@ class EpisodeAudioController extends Controller
|
|||
}
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -79,7 +73,7 @@ class EpisodeAudioController extends Controller
|
|||
$this->podcast = $podcast;
|
||||
|
||||
if (
|
||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -99,7 +93,7 @@ class EpisodeAudioController extends Controller
|
|||
|
||||
// check if podcast is already unlocked before any token validation
|
||||
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
|
||||
$this->episode->podcast->handle
|
||||
$this->episode->podcast->handle,
|
||||
)) instanceof Subscription) {
|
||||
// look for token as GET parameter
|
||||
if (($token = $this->request->getGet('token')) === null) {
|
||||
|
|
@ -114,9 +108,9 @@ class EpisodeAudioController extends Controller
|
|||
}
|
||||
|
||||
// check if there's a valid subscription for the provided token
|
||||
if (! ($subscription = (new SubscriptionModel())->validateSubscription(
|
||||
if (! ($subscription = new SubscriptionModel()->validateSubscription(
|
||||
$this->episode->podcast->handle,
|
||||
$token
|
||||
$token,
|
||||
)) instanceof Subscription) {
|
||||
return $this->response->setStatusCode(401, 'Invalid token!')
|
||||
->setJSON([
|
||||
|
|
@ -160,7 +154,7 @@ class EpisodeAudioController extends Controller
|
|||
$audioDuration,
|
||||
$this->episode->published_at->getTimestamp(),
|
||||
$serviceName,
|
||||
$subscription instanceof Subscription ? $subscription->id : null
|
||||
$subscription instanceof Subscription ? $subscription->id : null,
|
||||
);
|
||||
|
||||
$audioFileURI = new URI(service('file_manager')->getUrl($this->episode->audio->file_key));
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use App\Models\EpisodeModel;
|
|||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Modules\Analytics\AnalyticsTrait;
|
||||
use Modules\Fediverse\Entities\Actor;
|
||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||
|
|
@ -44,7 +44,7 @@ class EpisodeCommentController extends BaseController
|
|||
}
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ class EpisodeCommentController extends BaseController
|
|||
$this->actor = $podcast->actor;
|
||||
|
||||
if (
|
||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ class EpisodeCommentController extends BaseController
|
|||
$this->episode = $episode;
|
||||
|
||||
if (
|
||||
! ($comment = (new EpisodeCommentModel())->getCommentById($params[2])) instanceof EpisodeComment
|
||||
! ($comment = new EpisodeCommentModel()->getCommentById($params[2])) instanceof EpisodeComment
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -77,10 +77,7 @@ class EpisodeCommentController extends BaseController
|
|||
|
||||
public function view(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -96,12 +93,12 @@ class EpisodeCommentController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_comment_metatags($this->comment);
|
||||
$data = [
|
||||
'metatags' => get_episode_comment_metatags($this->comment),
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'episode' => $this->episode,
|
||||
'comment' => $this->comment,
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'episode' => $this->episode,
|
||||
'comment' => $this->comment,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
|
|
@ -119,7 +116,7 @@ class EpisodeCommentController extends BaseController
|
|||
return $cachedView;
|
||||
}
|
||||
|
||||
public function commentObject(): Response
|
||||
public function commentObject(): ResponseInterface
|
||||
{
|
||||
$commentObject = new CommentObject($this->comment);
|
||||
|
||||
|
|
@ -128,7 +125,7 @@ class EpisodeCommentController extends BaseController
|
|||
->setBody($commentObject->toJSON());
|
||||
}
|
||||
|
||||
public function replies(): Response
|
||||
public function replies(): ResponseInterface
|
||||
{
|
||||
/**
|
||||
* get comment replies
|
||||
|
|
@ -148,11 +145,9 @@ class EpisodeCommentController extends BaseController
|
|||
$pager = $commentReplies->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
if ($paginatedReplies !== null) {
|
||||
foreach ($paginatedReplies as $reply) {
|
||||
$replyObject = new CommentObject($reply);
|
||||
$orderedItems[] = $replyObject;
|
||||
}
|
||||
foreach ($paginatedReplies as $reply) {
|
||||
$replyObject = new CommentObject($reply);
|
||||
$orderedItems[] = $replyObject;
|
||||
}
|
||||
|
||||
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||
|
|
@ -163,7 +158,7 @@ class EpisodeCommentController extends BaseController
|
|||
->setBody($collection->toJSON());
|
||||
}
|
||||
|
||||
public function attemptLike(): RedirectResponse
|
||||
public function likeAction(): RedirectResponse
|
||||
{
|
||||
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
||||
return redirect()->back();
|
||||
|
|
@ -175,7 +170,7 @@ class EpisodeCommentController extends BaseController
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReply(): RedirectResponse
|
||||
public function replyAction(): RedirectResponse
|
||||
{
|
||||
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
||||
return redirect()->back();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ use App\Models\EpisodeModel;
|
|||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Embed;
|
||||
use Modules\Analytics\AnalyticsTrait;
|
||||
|
|
@ -42,7 +41,7 @@ class EpisodeController extends BaseController
|
|||
}
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -50,7 +49,7 @@ class EpisodeController extends BaseController
|
|||
$this->podcast = $podcast;
|
||||
|
||||
if (
|
||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -65,10 +64,7 @@ class EpisodeController extends BaseController
|
|||
|
||||
public function index(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -85,15 +81,14 @@ class EpisodeController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
helper('form');
|
||||
|
|
@ -113,10 +108,7 @@ class EpisodeController extends BaseController
|
|||
|
||||
public function activity(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -134,15 +126,14 @@ class EpisodeController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
helper('form');
|
||||
|
|
@ -162,10 +153,7 @@ class EpisodeController extends BaseController
|
|||
|
||||
public function chapters(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -183,13 +171,13 @@ class EpisodeController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
// get chapters from json file
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
// get chapters from json file
|
||||
if (isset($this->episode->chapters->file_key)) {
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
|
|
@ -199,9 +187,8 @@ class EpisodeController extends BaseController
|
|||
$data['chapters'] = $chapters;
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
helper('form');
|
||||
|
|
@ -221,10 +208,7 @@ class EpisodeController extends BaseController
|
|||
|
||||
public function transcript(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -242,13 +226,13 @@ class EpisodeController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
// get transcript from json file
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
// get transcript from json file
|
||||
if ($this->episode->transcript !== null) {
|
||||
$data['transcript'] = $this->episode->transcript;
|
||||
|
||||
|
|
@ -256,16 +240,15 @@ class EpisodeController extends BaseController
|
|||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
$transcriptJsonString = (string) $fileManager->getFileContents(
|
||||
$this->episode->transcript->json_key
|
||||
$this->episode->transcript->json_key,
|
||||
);
|
||||
|
||||
$data['captions'] = json_decode($transcriptJsonString, true);
|
||||
}
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
helper('form');
|
||||
|
|
@ -287,10 +270,7 @@ class EpisodeController extends BaseController
|
|||
{
|
||||
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
||||
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
|
||||
$session = service('session');
|
||||
|
||||
|
|
@ -322,9 +302,8 @@ class EpisodeController extends BaseController
|
|||
'themeData' => $themeData,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('embed', $data, [
|
||||
|
|
@ -381,7 +360,7 @@ class EpisodeController extends BaseController
|
|||
'<iframe src="' .
|
||||
$this->episode->embed_url .
|
||||
'" width="100%" height="' . config(
|
||||
Embed::class
|
||||
Embed::class,
|
||||
)->height . '" frameborder="0" scrolling="no"></iframe>',
|
||||
),
|
||||
);
|
||||
|
|
@ -392,7 +371,7 @@ class EpisodeController extends BaseController
|
|||
return $this->response->setXML($oembed);
|
||||
}
|
||||
|
||||
public function episodeObject(): Response
|
||||
public function episodeObject(): ResponseInterface
|
||||
{
|
||||
$podcastObject = new PodcastEpisode($this->episode);
|
||||
|
||||
|
|
@ -401,7 +380,7 @@ class EpisodeController extends BaseController
|
|||
->setBody($podcastObject->toJSON());
|
||||
}
|
||||
|
||||
public function comments(): Response
|
||||
public function comments(): ResponseInterface
|
||||
{
|
||||
/**
|
||||
* get comments: aggregated replies from posts referring to the episode
|
||||
|
|
@ -424,10 +403,8 @@ class EpisodeController extends BaseController
|
|||
$pager = $episodeComments->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
if ($paginatedComments !== null) {
|
||||
foreach ($paginatedComments as $comment) {
|
||||
$orderedItems[] = (new NoteObject($comment))->toArray();
|
||||
}
|
||||
foreach ($paginatedComments as $comment) {
|
||||
$orderedItems[] = new NoteObject($comment)->toArray();
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ class EpisodePreviewController extends BaseController
|
|||
}
|
||||
|
||||
// find episode by previewUUID
|
||||
$episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
|
||||
$episode = new EpisodeModel()
|
||||
->getEpisodeByPreviewId($params[0]);
|
||||
|
||||
if (! $episode instanceof Episode) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
|
|
@ -99,7 +100,7 @@ class EpisodePreviewController extends BaseController
|
|||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
$transcriptJsonString = (string) $fileManager->getFileContents(
|
||||
$this->episode->transcript->json_key
|
||||
$this->episode->transcript->json_key,
|
||||
);
|
||||
|
||||
$data['captions'] = json_decode($transcriptJsonString, true);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class FeedController extends Controller
|
|||
|
||||
public function index(string $podcastHandle): ResponseInterface
|
||||
{
|
||||
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
|
||||
$podcast = new PodcastModel()
|
||||
->where('handle', $podcastHandle)
|
||||
->first();
|
||||
if (! $podcast instanceof Podcast) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
|
|
@ -45,7 +46,7 @@ class FeedController extends Controller
|
|||
|
||||
if ($redirectToNewFeed && $podcast->new_feed_url !== null && filter_var(
|
||||
$podcast->new_feed_url,
|
||||
FILTER_VALIDATE_URL
|
||||
FILTER_VALIDATE_URL,
|
||||
) && $podcast->new_feed_url !== current_url()) {
|
||||
return redirect()->to($podcast->new_feed_url, 301);
|
||||
}
|
||||
|
|
@ -68,7 +69,8 @@ class FeedController extends Controller
|
|||
$subscription = null;
|
||||
$token = $this->request->getGet('token');
|
||||
if ($token) {
|
||||
$subscription = (new SubscriptionModel())->validateSubscription($podcastHandle, $token);
|
||||
$subscription = new SubscriptionModel()
|
||||
->validateSubscription($podcastHandle, $token);
|
||||
}
|
||||
|
||||
$cacheName = implode(
|
||||
|
|
@ -85,9 +87,8 @@ class FeedController extends Controller
|
|||
$found = get_rss_feed($podcast, $serviceSlug, $subscription, $token);
|
||||
|
||||
// The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($podcast->id);
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
|
||||
|
|
|
|||
|
|
@ -22,19 +22,20 @@ class HomeController extends BaseController
|
|||
{
|
||||
$sortOptions = ['activity', 'created_desc', 'created_asc'];
|
||||
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
|
||||
'sort'
|
||||
'sort',
|
||||
) : 'activity';
|
||||
|
||||
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
|
||||
$allPodcasts = new PodcastModel()
|
||||
->getAllPodcasts($sortBy);
|
||||
|
||||
// check if there's only one podcast to redirect user to it
|
||||
if (count($allPodcasts) === 1) {
|
||||
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
|
||||
}
|
||||
|
||||
set_home_metatags();
|
||||
// default behavior: list all podcasts on home page
|
||||
$data = [
|
||||
'metatags' => get_home_metatags(),
|
||||
'podcasts' => $allPodcasts,
|
||||
'sortBy' => $sortBy,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class MapController extends BaseController
|
|||
{
|
||||
$cacheName = 'episodes_markers';
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$episodes = (new EpisodeModel())
|
||||
$episodes = new EpisodeModel()
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
->where('location_geo is not')
|
||||
->findAll();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ class PageController extends BaseController
|
|||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$page = (new PageModel())->where('slug', $params[0])->first();
|
||||
$page = new PageModel()
|
||||
->where('slug', $params[0])->first();
|
||||
if (! $page instanceof Page) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -49,9 +50,9 @@ class PageController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($found = cache($cacheName))) {
|
||||
set_page_metatags($this->page);
|
||||
$data = [
|
||||
'metatags' => get_page_metatags($this->page),
|
||||
'page' => $this->page,
|
||||
'page' => $this->page,
|
||||
];
|
||||
|
||||
$found = view('pages/page', $data);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use App\Models\EpisodeModel;
|
|||
use App\Models\PodcastModel;
|
||||
use App\Models\PostModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Modules\Analytics\AnalyticsTrait;
|
||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||
|
|
@ -35,7 +35,7 @@ class PodcastController extends BaseController
|
|||
}
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ class PodcastController extends BaseController
|
|||
return $this->{$method}(...$params);
|
||||
}
|
||||
|
||||
public function podcastActor(): Response
|
||||
public function podcastActor(): ResponseInterface
|
||||
{
|
||||
$podcastActor = new PodcastActor($this->podcast);
|
||||
|
||||
|
|
@ -58,10 +58,7 @@ class PodcastController extends BaseController
|
|||
|
||||
public function activity(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -78,10 +75,11 @@ class PodcastController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_podcast_metatags($this->podcast, 'activity');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'activity'),
|
||||
'podcast' => $this->podcast,
|
||||
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
|
||||
'podcast' => $this->podcast,
|
||||
'posts' => new PostModel()
|
||||
->getActorPublishedPosts($this->podcast->actor_id),
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
|
|
@ -91,9 +89,8 @@ class PodcastController extends BaseController
|
|||
return view('podcast/activity', $data);
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
return view('podcast/activity', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||
|
|
@ -106,10 +103,7 @@ class PodcastController extends BaseController
|
|||
|
||||
public function about(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -126,12 +120,13 @@ class PodcastController extends BaseController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
$stats = (new EpisodeModel())->getPodcastStats($this->podcast->id);
|
||||
$stats = new EpisodeModel()
|
||||
->getPodcastStats($this->podcast->id);
|
||||
|
||||
set_podcast_metatags($this->podcast, 'about');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'about'),
|
||||
'podcast' => $this->podcast,
|
||||
'stats' => $stats,
|
||||
'podcast' => $this->podcast,
|
||||
'stats' => $stats,
|
||||
];
|
||||
|
||||
// // if user is logged in then send to the authenticated activity view
|
||||
|
|
@ -141,9 +136,8 @@ class PodcastController extends BaseController
|
|||
return view('podcast/about', $data);
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
|
||||
return view('podcast/about', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||
|
|
@ -156,16 +150,14 @@ class PodcastController extends BaseController
|
|||
|
||||
public function episodes(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
$yearQuery = $this->request->getGet('year');
|
||||
$seasonQuery = $this->request->getGet('season');
|
||||
|
||||
if (! $yearQuery && ! $seasonQuery) {
|
||||
$defaultQuery = (new PodcastModel())->getDefaultQuery($this->podcast->id);
|
||||
$defaultQuery = new PodcastModel()
|
||||
->getDefaultQuery($this->podcast->id);
|
||||
if ($defaultQuery) {
|
||||
if ($defaultQuery['type'] === 'season') {
|
||||
$seasonQuery = $defaultQuery['data']['season_number'];
|
||||
|
|
@ -245,26 +237,21 @@ class PodcastController extends BaseController
|
|||
];
|
||||
}
|
||||
|
||||
set_podcast_metatags($this->podcast, 'episodes');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'episodes'),
|
||||
'podcast' => $this->podcast,
|
||||
'episodesNav' => $episodesNavigation,
|
||||
'activeQuery' => $activeQuery,
|
||||
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
|
||||
$this->podcast->id,
|
||||
$this->podcast->type,
|
||||
$yearQuery,
|
||||
$seasonQuery,
|
||||
),
|
||||
'episodes' => new EpisodeModel()
|
||||
->getPodcastEpisodes($this->podcast->id, $this->podcast->type, $yearQuery, $seasonQuery),
|
||||
];
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
return view('podcast/episodes', $data);
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||
return view('podcast/episodes', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
|
|
@ -274,7 +261,7 @@ class PodcastController extends BaseController
|
|||
return $cachedView;
|
||||
}
|
||||
|
||||
public function episodeCollection(): Response
|
||||
public function episodeCollection(): ResponseInterface
|
||||
{
|
||||
if ($this->podcast->type === 'serial') {
|
||||
// podcast is serial
|
||||
|
|
@ -298,10 +285,8 @@ class PodcastController extends BaseController
|
|||
$pager = $episodes->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
if ($paginatedEpisodes !== null) {
|
||||
foreach ($paginatedEpisodes as $episode) {
|
||||
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
|
||||
}
|
||||
foreach ($paginatedEpisodes as $episode) {
|
||||
$orderedItems[] = new PodcastEpisode($episode)->toArray();
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
|
|
@ -315,9 +300,9 @@ class PodcastController extends BaseController
|
|||
|
||||
public function links(): string
|
||||
{
|
||||
set_podcast_metatags($this->podcast, 'links');
|
||||
return view('podcast/links', [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'links'),
|
||||
'podcast' => $this->podcast,
|
||||
'podcast' => $this->podcast,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use CodeIgniter\HTTP\URI;
|
|||
use CodeIgniter\I18n\Time;
|
||||
use Modules\Analytics\AnalyticsTrait;
|
||||
use Modules\Fediverse\Controllers\PostController as FediversePostController;
|
||||
use Override;
|
||||
|
||||
class PostController extends FediversePostController
|
||||
{
|
||||
|
|
@ -41,10 +42,12 @@ class PostController extends FediversePostController
|
|||
*/
|
||||
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
|
||||
|
||||
#[Override]
|
||||
public function _remap(string $method, string ...$params): mixed
|
||||
{
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -59,7 +62,7 @@ class PostController extends FediversePostController
|
|||
}
|
||||
|
||||
if (
|
||||
! ($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
|
||||
! ($post = new PostModel()->getPostById($params[1])) instanceof CastopodPost
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
@ -79,10 +82,7 @@ class PostController extends FediversePostController
|
|||
|
||||
public function view(): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -97,10 +97,10 @@ class PostController extends FediversePostController
|
|||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_post_metatags($this->post);
|
||||
$data = [
|
||||
'metatags' => get_post_metatags($this->post),
|
||||
'post' => $this->post,
|
||||
'podcast' => $this->podcast,
|
||||
'post' => $this->post,
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
|
|
@ -118,7 +118,8 @@ class PostController extends FediversePostController
|
|||
return $cachedView;
|
||||
}
|
||||
|
||||
public function attemptCreate(): RedirectResponse
|
||||
#[Override]
|
||||
public function createAction(): RedirectResponse
|
||||
{
|
||||
$rules = [
|
||||
'message' => 'required|max_length[500]',
|
||||
|
|
@ -147,7 +148,7 @@ class PostController extends FediversePostController
|
|||
if (
|
||||
$episodeUri &&
|
||||
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
|
||||
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
|
||||
($episode = new EpisodeModel()->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
|
||||
) {
|
||||
$newPost->episode_id = $episode->id;
|
||||
}
|
||||
|
|
@ -169,7 +170,8 @@ class PostController extends FediversePostController
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReply(): RedirectResponse
|
||||
#[Override]
|
||||
public function replyAction(): RedirectResponse
|
||||
{
|
||||
$rules = [
|
||||
'message' => 'required|max_length[500]',
|
||||
|
|
@ -209,21 +211,24 @@ class PostController extends FediversePostController
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptFavourite(): RedirectResponse
|
||||
#[Override]
|
||||
public function favouriteAction(): RedirectResponse
|
||||
{
|
||||
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReblog(): RedirectResponse
|
||||
#[Override]
|
||||
public function reblogAction(): RedirectResponse
|
||||
{
|
||||
(new PostModel())->toggleReblog(interact_as_actor(), $this->post);
|
||||
new PostModel()
|
||||
->toggleReblog(interact_as_actor(), $this->post);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptAction(): RedirectResponse
|
||||
public function action(): RedirectResponse
|
||||
{
|
||||
$rules = [
|
||||
'action' => 'required|in_list[favourite,reblog,reply]',
|
||||
|
|
@ -240,9 +245,9 @@ class PostController extends FediversePostController
|
|||
|
||||
$action = $validData['action'];
|
||||
return match ($action) {
|
||||
'favourite' => $this->attemptFavourite(),
|
||||
'reblog' => $this->attemptReblog(),
|
||||
'reply' => $this->attemptReply(),
|
||||
'favourite' => $this->favouriteAction(),
|
||||
'reblog' => $this->reblogAction(),
|
||||
'reply' => $this->replyAction(),
|
||||
default => redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
|
|
@ -250,19 +255,16 @@ class PostController extends FediversePostController
|
|||
};
|
||||
}
|
||||
|
||||
public function remoteAction(string $action): string
|
||||
public function remoteActionView(string $action): string
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
|
||||
set_remote_actions_metatags($this->post, $action);
|
||||
$data = [
|
||||
'metatags' => get_remote_actions_metatags($this->post, $action),
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'post' => $this->post,
|
||||
'action' => $action,
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'post' => $this->post,
|
||||
'action' => $action,
|
||||
];
|
||||
|
||||
helper('form');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class WebmanifestController extends Controller
|
|||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
final public const THEME_COLORS = [
|
||||
final public const array THEME_COLORS = [
|
||||
'pine' => [
|
||||
'theme' => '#009486',
|
||||
'background' => '#F0F9F8',
|
||||
|
|
@ -82,7 +82,7 @@ class WebmanifestController extends Controller
|
|||
public function podcastManifest(string $podcastHandle): ResponseInterface
|
||||
{
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) instanceof Podcast
|
||||
! ($podcast = new PodcastModel()->getPodcastByHandle($podcastHandle)) instanceof Podcast
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddCategories extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -45,6 +48,7 @@ class AddCategories extends BaseMigration
|
|||
$this->forge->createTable('categories');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('categories');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddLanguages extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -31,6 +34,7 @@ class AddLanguages extends BaseMigration
|
|||
$this->forge->createTable('languages');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('languages');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcasts extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -205,6 +208,7 @@ class AddPodcasts extends BaseMigration
|
|||
$this->forge->createTable('podcasts');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('podcasts');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddEpisodes extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -171,6 +174,7 @@ class AddEpisodes extends BaseMigration
|
|||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('episodes');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPlatforms extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -41,12 +44,13 @@ class AddPlatforms extends BaseMigration
|
|||
]);
|
||||
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()',
|
||||
);
|
||||
$this->forge->addPrimaryKey('slug');
|
||||
$this->forge->createTable('platforms');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('platforms');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsPlatforms extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -52,6 +55,7 @@ class AddPodcastsPlatforms extends BaseMigration
|
|||
$this->forge->createTable('podcasts_platforms');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('podcasts_platforms');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddEpisodeComments extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -71,6 +74,7 @@ class AddEpisodeComments extends BaseMigration
|
|||
$this->forge->createTable('episode_comments');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('episode_comments');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddLikes extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -34,6 +37,7 @@ class AddLikes extends BaseMigration
|
|||
$this->forge->createTable('likes');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('likes');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPages extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -48,6 +51,7 @@ class AddPages extends BaseMigration
|
|||
$this->forge->createTable('pages');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('pages');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsCategories extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -32,6 +35,7 @@ class AddPodcastsCategories extends BaseMigration
|
|||
$this->forge->createTable('podcasts_categories');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('podcasts_categories');
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddClips extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -94,6 +97,7 @@ class AddClips extends BaseMigration
|
|||
$this->forge->createTable('clips');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('clips');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPersons extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -67,6 +70,7 @@ class AddPersons extends BaseMigration
|
|||
$this->forge->createTable('persons');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('persons');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsPersons extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -46,6 +49,7 @@ class AddPodcastsPersons extends BaseMigration
|
|||
$this->forge->createTable('podcasts_persons');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('podcasts_persons');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddEpisodesPersons extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -51,6 +54,7 @@ class AddEpisodesPersons extends BaseMigration
|
|||
$this->forge->createTable('episodes_persons');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('episodes_persons');
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddCreditsView extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
// Creates View for credit UNION query
|
||||
|
|
@ -37,6 +40,7 @@ class AddCreditsView extends BaseMigration
|
|||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$viewName = $this->db->prefixTable('credits');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddEpisodeIdToPosts extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
@ -33,11 +36,12 @@ class AddEpisodeIdToPosts extends BaseMigration
|
|||
'id',
|
||||
'',
|
||||
'CASCADE',
|
||||
$prefix . 'fediverse_posts_episode_id_foreign'
|
||||
$prefix . 'fediverse_posts_episode_id_foreign',
|
||||
);
|
||||
$this->forge->processIndexes('fediverse_posts');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddCreatedByToPosts extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
@ -33,11 +36,12 @@ class AddCreatedByToPosts extends BaseMigration
|
|||
'id',
|
||||
'',
|
||||
'CASCADE',
|
||||
$prefix . 'fediverse_posts_created_by_foreign'
|
||||
$prefix . 'fediverse_posts_created_by_foreign',
|
||||
);
|
||||
$this->forge->processIndexes('fediverse_posts');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddFullTextSearchIndexes extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
@ -31,6 +34,7 @@ class AddFullTextSearchIndexes extends BaseMigration
|
|||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddEpisodePreviewId extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$fields = [
|
||||
|
|
@ -28,6 +31,7 @@ class AddEpisodePreviewId extends BaseMigration
|
|||
$this->db->query($uniquePreviewId);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$fields = ['preview_id'];
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$fields = [
|
||||
|
|
@ -28,6 +31,7 @@ class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
|
|||
$this->forge->addColumn('podcasts', $fields);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$fields = ['is_owner_email_removed_from_feed'];
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsMediumField extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$fields = [
|
||||
|
|
@ -28,6 +31,7 @@ class AddPodcastsMediumField extends BaseMigration
|
|||
$this->forge->addColumn('podcasts', $fields);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$fields = ['medium'];
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class AddPodcastsVerifyTxtField extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$fields = [
|
||||
|
|
@ -27,6 +30,7 @@ class AddPodcastsVerifyTxtField extends BaseMigration
|
|||
$this->forge->addColumn('podcasts', $fields);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropColumn('podcasts', 'verify_txt');
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ declare(strict_types=1);
|
|||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
use Override;
|
||||
|
||||
class RefactorPlatforms extends Migration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
|
|
@ -80,6 +82,7 @@ class RefactorPlatforms extends Migration
|
|||
$this->forge->renameTable('platforms_temp', 'platforms');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
// delete platforms
|
||||
|
|
@ -111,7 +114,7 @@ class RefactorPlatforms extends Migration
|
|||
]);
|
||||
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()',
|
||||
);
|
||||
$this->forge->addPrimaryKey('slug');
|
||||
$this->forge->createTable('platforms');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* CodeIgniter 4.5.1 introduces new DataCaster class that breaks deserialization of import queue tasks.
|
||||
|
|
@ -12,11 +13,13 @@ use CodeIgniter\Database\Migration;
|
|||
*/
|
||||
class ClearImportQueue extends Migration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
service('settings')->forget('Import.queue');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
// nothing
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Class AddPodcastsMediumField adds medium field to podcast table in database
|
||||
*
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class DropDeprecatedPodcastsFields extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
// TODO: migrate data
|
||||
|
||||
$this->forge->dropColumn(
|
||||
'podcasts',
|
||||
'episode_description_footer_markdown,episode_description_footer_html,is_owner_email_removed_from_feed,medium,payment_pointer,verify_txt,custom_rss,partner_id,partner_link_url,partner_image_url',
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$fields = [
|
||||
'episode_description_footer_markdown' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'episode_description_footer_html' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'is_owner_email_removed_from_feed' => [
|
||||
'type' => 'BOOLEAN',
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'after' => 'owner_email',
|
||||
],
|
||||
'medium' => [
|
||||
'type' => "ENUM('podcast','music','audiobook')",
|
||||
'null' => false,
|
||||
'default' => 'podcast',
|
||||
'after' => 'type',
|
||||
],
|
||||
'payment_pointer' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
'comment' => 'Wallet address for Web Monetization payments',
|
||||
'null' => true,
|
||||
],
|
||||
'verify_txt' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
'after' => 'location_osm',
|
||||
],
|
||||
'custom_rss' => [
|
||||
'type' => 'JSON',
|
||||
'null' => true,
|
||||
],
|
||||
'partner_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 32,
|
||||
'null' => true,
|
||||
],
|
||||
'partner_link_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 512,
|
||||
'null' => true,
|
||||
],
|
||||
'partner_image_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 512,
|
||||
'null' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$this->forge->addColumn('podcasts', $fields);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Class AddPodcastsMediumField adds medium field to podcast table in database
|
||||
*
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use Override;
|
||||
|
||||
class DropDeprecatedEpisodesFields extends BaseMigration
|
||||
{
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->dropColumn('episodes', 'custom_rss');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
$fields = [
|
||||
'custom_rss' => [
|
||||
'type' => 'JSON',
|
||||
'null' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$this->forge->addColumn('episodes', $fields);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ namespace App\Database\Migrations;
|
|||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Migration;
|
||||
use Override;
|
||||
|
||||
class BaseMigration extends Migration
|
||||
{
|
||||
|
|
@ -24,10 +25,12 @@ class BaseMigration extends Migration
|
|||
*/
|
||||
protected $db;
|
||||
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
|||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Override;
|
||||
|
||||
class AppSeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$this->call('CategorySeeder');
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
|||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Override;
|
||||
|
||||
class CategorySeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$data = [
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
|||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Override;
|
||||
|
||||
class DevSeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$this->call('CategorySeeder');
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ namespace App\Database\Seeds;
|
|||
use CodeIgniter\Database\Seeder;
|
||||
use CodeIgniter\Shield\Entities\User;
|
||||
use Modules\Auth\Models\UserModel;
|
||||
use Override;
|
||||
|
||||
class DevSuperadminSeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
if ((new UserModel())->where('is_owner', true)->first() instanceof User) {
|
||||
if (new UserModel()->where('is_owner', true)->first() instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ use CodeIgniter\Database\Seeder;
|
|||
use Exception;
|
||||
use GeoIp2\Database\Reader;
|
||||
use GeoIp2\Exception\AddressNotFoundException;
|
||||
use Override;
|
||||
|
||||
class FakePodcastsAnalyticsSeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$jsonUserAgents = json_decode(
|
||||
|
|
@ -41,13 +43,14 @@ class FakePodcastsAnalyticsSeeder extends Seeder
|
|||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
$podcast = (new PodcastModel())->first();
|
||||
$podcast = new PodcastModel()
|
||||
->first();
|
||||
|
||||
if (! $podcast instanceof Podcast) {
|
||||
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
||||
}
|
||||
|
||||
$firstEpisode = (new EpisodeModel())
|
||||
$firstEpisode = new EpisodeModel()
|
||||
->selectMin('published_at')
|
||||
->first();
|
||||
|
||||
|
|
@ -67,7 +70,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
|
|||
$analyticsPodcastsByPlayer = [];
|
||||
$analyticsPodcastsByRegion = [];
|
||||
|
||||
$episodes = (new EpisodeModel())
|
||||
$episodes = new EpisodeModel()
|
||||
->where('podcast_id', $podcast->id)
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
->findAll();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use App\Models\EpisodeModel;
|
|||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Exception;
|
||||
use Override;
|
||||
|
||||
class FakeWebsiteAnalyticsSeeder extends Seeder
|
||||
{
|
||||
|
|
@ -181,15 +182,17 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
|
|||
'WOSBrowser',
|
||||
];
|
||||
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$podcast = (new PodcastModel())->first();
|
||||
$podcast = new PodcastModel()
|
||||
->first();
|
||||
|
||||
if (! $podcast instanceof Podcast) {
|
||||
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
||||
}
|
||||
|
||||
$firstEpisode = (new EpisodeModel())
|
||||
$firstEpisode = new EpisodeModel()
|
||||
->selectMin('published_at')
|
||||
->first();
|
||||
|
||||
|
|
@ -206,7 +209,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
|
|||
$websiteByEntryPage = [];
|
||||
$websiteByReferer = [];
|
||||
|
||||
$episodes = (new EpisodeModel())
|
||||
$episodes = new EpisodeModel()
|
||||
->where('podcast_id', $podcast->id)
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
->findAll();
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ declare(strict_types=1);
|
|||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Override;
|
||||
|
||||
class LanguageSeeder extends Seeder
|
||||
{
|
||||
#[Override]
|
||||
public function run(): void
|
||||
{
|
||||
$data = [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace App\Entities;
|
|||
|
||||
use App\Models\PodcastModel;
|
||||
use Modules\Fediverse\Entities\Actor as FediverseActor;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property Podcast|null $podcast
|
||||
|
|
@ -31,12 +32,14 @@ class Actor extends FediverseActor
|
|||
public function getPodcast(): ?Podcast
|
||||
{
|
||||
if (! $this->podcast instanceof Podcast) {
|
||||
$this->podcast = (new PodcastModel())->getPodcastByActorId($this->id);
|
||||
$this->podcast = new PodcastModel()
|
||||
->getPodcastByActorId($this->id);
|
||||
}
|
||||
|
||||
return $this->podcast;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getAvatarImageUrl(): string
|
||||
{
|
||||
if ($this->podcast instanceof Podcast) {
|
||||
|
|
@ -46,6 +49,7 @@ class Actor extends FediverseActor
|
|||
return parent::getAvatarImageUrl();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getAvatarImageMimetype(): string
|
||||
{
|
||||
if ($this->podcast instanceof Podcast) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use CodeIgniter\Entity\Entity;
|
|||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int|null $parent_id
|
||||
* @property ?int $parent_id
|
||||
* @property Category|null $parent
|
||||
* @property string $code
|
||||
* @property string $apple_category
|
||||
|
|
@ -42,6 +42,7 @@ class Category extends Entity
|
|||
return null;
|
||||
}
|
||||
|
||||
return (new CategoryModel())->getCategoryById($this->parent_id);
|
||||
return new CategoryModel()
|
||||
->getCategoryById($this->parent_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use Modules\Media\Models\MediaModel;
|
|||
* @property Episode $episode
|
||||
* @property string $title
|
||||
* @property double $start_time
|
||||
* @property double $end_time
|
||||
* @property ?double $end_time
|
||||
* @property double $duration
|
||||
* @property string $type
|
||||
* @property int|null $media_id
|
||||
|
|
@ -81,14 +81,6 @@ class BaseClip extends Entity
|
|||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
public function getJobDuration(): ?int
|
||||
{
|
||||
if ($this->job_duration === null && $this->job_started_at && $this->job_ended_at) {
|
||||
|
|
@ -110,18 +102,21 @@ class BaseClip extends Entity
|
|||
|
||||
public function getPodcast(): ?Podcast
|
||||
{
|
||||
return (new PodcastModel())->getPodcastById($this->podcast_id);
|
||||
return new PodcastModel()
|
||||
->getPodcastById($this->podcast_id);
|
||||
}
|
||||
|
||||
public function getEpisode(): ?Episode
|
||||
{
|
||||
return (new EpisodeModel())->getEpisodeById($this->episode_id);
|
||||
return new EpisodeModel()
|
||||
->getEpisodeById($this->episode_id);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
return (new UserModel())->find($this->created_by);
|
||||
return new UserModel()
|
||||
->find($this->created_by);
|
||||
}
|
||||
|
||||
public function setMedia(File $file, string $fileKey): static
|
||||
|
|
@ -131,7 +126,8 @@ class BaseClip extends Entity
|
|||
->setFile($file);
|
||||
$this->getMedia()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('audio'))->updateMedia($this->getMedia());
|
||||
new MediaModel('audio')
|
||||
->updateMedia($this->getMedia());
|
||||
} else {
|
||||
$media = new Audio([
|
||||
'file_key' => $fileKey,
|
||||
|
|
@ -142,7 +138,7 @@ class BaseClip extends Entity
|
|||
]);
|
||||
$media->setFile($file);
|
||||
|
||||
$this->attributes['media_id'] = (new MediaModel())->saveMedia($media);
|
||||
$this->attributes['media_id'] = new MediaModel()->saveMedia($media);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -151,7 +147,8 @@ class BaseClip extends Entity
|
|||
public function getMedia(): Audio | Video | null
|
||||
{
|
||||
if ($this->media_id !== null && $this->media === null) {
|
||||
$this->media = (new MediaModel($this->type))->getMediaById($this->media_id);
|
||||
$this->media = new MediaModel($this->type)
|
||||
->getMediaById($this->media_id);
|
||||
}
|
||||
|
||||
return $this->media;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace App\Entities\Clip;
|
|||
use CodeIgniter\Files\File;
|
||||
use Modules\Media\Entities\Video;
|
||||
use Modules\Media\Models\MediaModel;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property array{name:string,preview:string} $theme
|
||||
|
|
@ -25,7 +26,7 @@ class VideoClip extends BaseClip
|
|||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
public function __construct(?array $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ class VideoClip extends BaseClip
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $theme
|
||||
* @param array{name:string,preview:string} $theme
|
||||
*/
|
||||
public function setTheme(array $theme): self
|
||||
{
|
||||
|
|
@ -63,6 +64,7 @@ class VideoClip extends BaseClip
|
|||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function setMedia(File $file, string $fileKey): static
|
||||
{
|
||||
if ($this->attributes['media_id'] !== null) {
|
||||
|
|
@ -79,7 +81,7 @@ class VideoClip extends BaseClip
|
|||
]);
|
||||
$video->setFile($file);
|
||||
|
||||
$this->attributes['media_id'] = (new MediaModel('video'))->saveMedia($video);
|
||||
$this->attributes['media_id'] = new MediaModel('video')->saveMedia($video);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ class Credit extends Entity
|
|||
public function getPerson(): ?Person
|
||||
{
|
||||
if (! $this->person instanceof Person) {
|
||||
$this->person = (new PersonModel())->getPersonById($this->person_id);
|
||||
$this->person = new PersonModel()
|
||||
->getPersonById($this->person_id);
|
||||
}
|
||||
|
||||
return $this->person;
|
||||
|
|
@ -65,7 +66,8 @@ class Credit extends Entity
|
|||
public function getPodcast(): ?Podcast
|
||||
{
|
||||
if (! $this->podcast instanceof Podcast) {
|
||||
$this->podcast = (new PodcastModel())->getPodcastById($this->podcast_id);
|
||||
$this->podcast = new PodcastModel()
|
||||
->getPodcastById($this->podcast_id);
|
||||
}
|
||||
|
||||
return $this->podcast;
|
||||
|
|
@ -78,7 +80,8 @@ class Credit extends Entity
|
|||
}
|
||||
|
||||
if (! $this->episode instanceof Episode) {
|
||||
$this->episode = (new EpisodeModel())->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
|
||||
$this->episode = new EpisodeModel()
|
||||
->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
|
||||
}
|
||||
|
||||
return $this->episode;
|
||||
|
|
@ -86,6 +89,10 @@ class Credit extends Entity
|
|||
|
||||
public function getGroupLabel(): string
|
||||
{
|
||||
if ($this->person_group === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @var string */
|
||||
return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace App\Entities;
|
||||
|
||||
use App\Entities\Clip\Soundbite;
|
||||
use App\Libraries\SimpleRSSElement;
|
||||
use App\Models\ClipModel;
|
||||
use App\Models\EpisodeCommentModel;
|
||||
use App\Models\EpisodeModel;
|
||||
|
|
@ -29,13 +28,12 @@ use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
|||
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
|
||||
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
use Modules\Analytics\OP3;
|
||||
use Modules\Media\Entities\Audio;
|
||||
use Modules\Media\Entities\Chapters;
|
||||
use Modules\Media\Entities\Image;
|
||||
use Modules\Media\Entities\Transcript;
|
||||
use Modules\Media\Models\MediaModel;
|
||||
use SimpleXMLElement;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
|
|
@ -64,16 +62,14 @@ use SimpleXMLElement;
|
|||
* @property Chapters|null $chapters
|
||||
* @property string|null $chapters_remote_url
|
||||
* @property string|null $parental_advisory
|
||||
* @property int $number
|
||||
* @property int|null $season_number
|
||||
* @property ?int $number
|
||||
* @property ?int $season_number
|
||||
* @property string $type
|
||||
* @property bool $is_blocked
|
||||
* @property Location|null $location
|
||||
* @property string|null $location_name
|
||||
* @property string|null $location_geo
|
||||
* @property string|null $location_osm
|
||||
* @property array<string|int,mixed>|null $custom_rss
|
||||
* @property string $custom_rss_string
|
||||
* @property bool $is_published_on_hubs
|
||||
* @property int $downloads_count
|
||||
* @property int $posts_count
|
||||
|
|
@ -93,19 +89,19 @@ use SimpleXMLElement;
|
|||
*/
|
||||
class Episode extends Entity
|
||||
{
|
||||
protected Podcast $podcast;
|
||||
public string $link = '';
|
||||
|
||||
protected string $link;
|
||||
public string $audio_url = '';
|
||||
|
||||
public string $audio_web_url = '';
|
||||
|
||||
public string $audio_opengraph_url = '';
|
||||
|
||||
protected Podcast $podcast;
|
||||
|
||||
protected ?Audio $audio = null;
|
||||
|
||||
protected string $audio_url;
|
||||
|
||||
protected string $audio_web_url;
|
||||
|
||||
protected string $audio_opengraph_url;
|
||||
|
||||
protected string $embed_url;
|
||||
protected string $embed_url = '';
|
||||
|
||||
protected ?Image $cover = null;
|
||||
|
||||
|
|
@ -137,8 +133,6 @@ class Episode extends Entity
|
|||
|
||||
protected ?Location $location = null;
|
||||
|
||||
protected string $custom_rss_string;
|
||||
|
||||
protected ?string $publication_status = null;
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +167,6 @@ class Episode extends Entity
|
|||
'location_name' => '?string',
|
||||
'location_geo' => '?string',
|
||||
'location_osm' => '?string',
|
||||
'custom_rss' => '?json-array',
|
||||
'is_published_on_hubs' => 'boolean',
|
||||
'downloads_count' => 'integer',
|
||||
'posts_count' => 'integer',
|
||||
|
|
@ -183,7 +176,32 @@ class Episode extends Entity
|
|||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
public function setCover(UploadedFile | File $file = null): self
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
#[Override]
|
||||
public function injectRawData(array $data): static
|
||||
{
|
||||
parent::injectRawData($data);
|
||||
|
||||
$this->link = url_to('episode', esc($this->getPodcast()->handle, 'url'), esc($this->attributes['slug'], 'url'));
|
||||
|
||||
$this->audio_url = url_to(
|
||||
'episode-audio',
|
||||
$this->getPodcast()
|
||||
->handle,
|
||||
$this->slug,
|
||||
$this->getAudio()
|
||||
->file_extension,
|
||||
);
|
||||
|
||||
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
|
||||
$this->audio_web_url = $this->audio_url . '?_from=-+Website+-';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCover(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -194,7 +212,8 @@ class Episode extends Entity
|
|||
->setFile($file);
|
||||
$this->getCover()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('image'))->updateMedia($this->getCover());
|
||||
new MediaModel('image')
|
||||
->updateMedia($this->getCover());
|
||||
} else {
|
||||
$cover = new Image([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
|
||||
|
|
@ -205,7 +224,7 @@ class Episode extends Entity
|
|||
]);
|
||||
$cover->setFile($file);
|
||||
|
||||
$this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
|
||||
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -224,12 +243,13 @@ class Episode extends Entity
|
|||
return $this->cover;
|
||||
}
|
||||
|
||||
$this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
|
||||
$this->cover = new MediaModel('image')
|
||||
->getMediaById($this->cover_id);
|
||||
|
||||
return $this->cover;
|
||||
}
|
||||
|
||||
public function setAudio(UploadedFile | File $file = null): self
|
||||
public function setAudio(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -240,7 +260,8 @@ class Episode extends Entity
|
|||
->setFile($file);
|
||||
$this->getAudio()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('audio'))->updateMedia($this->getAudio());
|
||||
new MediaModel('audio')
|
||||
->updateMedia($this->getAudio());
|
||||
} else {
|
||||
$audio = new Audio([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
|
||||
|
|
@ -251,7 +272,7 @@ class Episode extends Entity
|
|||
]);
|
||||
$audio->setFile($file);
|
||||
|
||||
$this->attributes['audio_id'] = (new MediaModel())->saveMedia($audio);
|
||||
$this->attributes['audio_id'] = new MediaModel()->saveMedia($audio);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -260,13 +281,14 @@ class Episode extends Entity
|
|||
public function getAudio(): Audio
|
||||
{
|
||||
if (! $this->audio instanceof Audio) {
|
||||
$this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id);
|
||||
$this->audio = new MediaModel('audio')
|
||||
->getMediaById($this->audio_id);
|
||||
}
|
||||
|
||||
return $this->audio;
|
||||
}
|
||||
|
||||
public function setTranscript(UploadedFile | File $file = null): self
|
||||
public function setTranscript(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -277,7 +299,8 @@ class Episode extends Entity
|
|||
->setFile($file);
|
||||
$this->getTranscript()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('transcript'))->updateMedia($this->getTranscript());
|
||||
new MediaModel('transcript')
|
||||
->updateMedia($this->getTranscript());
|
||||
} else {
|
||||
$transcript = new Transcript([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
|
||||
|
|
@ -288,7 +311,7 @@ class Episode extends Entity
|
|||
]);
|
||||
$transcript->setFile($file);
|
||||
|
||||
$this->attributes['transcript_id'] = (new MediaModel('transcript'))->saveMedia($transcript);
|
||||
$this->attributes['transcript_id'] = new MediaModel('transcript')->saveMedia($transcript);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -297,13 +320,14 @@ class Episode extends Entity
|
|||
public function getTranscript(): ?Transcript
|
||||
{
|
||||
if ($this->transcript_id !== null && ! $this->transcript instanceof Transcript) {
|
||||
$this->transcript = (new MediaModel('transcript'))->getMediaById($this->transcript_id);
|
||||
$this->transcript = new MediaModel('transcript')
|
||||
->getMediaById($this->transcript_id);
|
||||
}
|
||||
|
||||
return $this->transcript;
|
||||
}
|
||||
|
||||
public function setChapters(UploadedFile | File $file = null): self
|
||||
public function setChapters(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -314,7 +338,8 @@ class Episode extends Entity
|
|||
->setFile($file);
|
||||
$this->getChapters()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('chapters'))->updateMedia($this->getChapters());
|
||||
new MediaModel('chapters')
|
||||
->updateMedia($this->getChapters());
|
||||
} else {
|
||||
$chapters = new Chapters([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
|
||||
|
|
@ -325,7 +350,7 @@ class Episode extends Entity
|
|||
]);
|
||||
$chapters->setFile($file);
|
||||
|
||||
$this->attributes['chapters_id'] = (new MediaModel('chapters'))->saveMedia($chapters);
|
||||
$this->attributes['chapters_id'] = new MediaModel('chapters')->saveMedia($chapters);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -334,46 +359,13 @@ class Episode extends Entity
|
|||
public function getChapters(): ?Chapters
|
||||
{
|
||||
if ($this->chapters_id !== null && ! $this->chapters instanceof Chapters) {
|
||||
$this->chapters = (new MediaModel('chapters'))->getMediaById($this->chapters_id);
|
||||
$this->chapters = new MediaModel('chapters')
|
||||
->getMediaById($this->chapters_id);
|
||||
}
|
||||
|
||||
return $this->chapters;
|
||||
}
|
||||
|
||||
public function getAudioUrl(): string
|
||||
{
|
||||
$audioURL = url_to(
|
||||
'episode-audio',
|
||||
$this->getPodcast()
|
||||
->handle,
|
||||
$this->slug,
|
||||
$this->getAudio()
|
||||
->file_extension
|
||||
);
|
||||
|
||||
// Wrap episode url with OP3 if episode is public and OP3 is enabled on this podcast
|
||||
if (! $this->is_premium && service('settings')->get(
|
||||
'Analytics.enableOP3',
|
||||
'podcast:' . $this->podcast_id
|
||||
)) {
|
||||
$op3 = new OP3(config('Analytics')->OP3);
|
||||
|
||||
return $op3->wrap($audioURL, $this);
|
||||
}
|
||||
|
||||
return $audioURL;
|
||||
}
|
||||
|
||||
public function getAudioWebUrl(): string
|
||||
{
|
||||
return $this->getAudioUrl() . '?_from=-+Website+-';
|
||||
}
|
||||
|
||||
public function getAudioOpengraphUrl(): string
|
||||
{
|
||||
return $this->getAudioUrl() . '?_from=-+Open+Graph+-';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null.
|
||||
*/
|
||||
|
|
@ -406,7 +398,8 @@ class Episode extends Entity
|
|||
public function getPersons(): array
|
||||
{
|
||||
if ($this->persons === null) {
|
||||
$this->persons = (new PersonModel())->getEpisodePersons($this->podcast_id, $this->id);
|
||||
$this->persons = new PersonModel()
|
||||
->getEpisodePersons($this->podcast_id, $this->id);
|
||||
}
|
||||
|
||||
return $this->persons;
|
||||
|
|
@ -420,7 +413,8 @@ class Episode extends Entity
|
|||
public function getSoundbites(): array
|
||||
{
|
||||
if ($this->soundbites === null) {
|
||||
$this->soundbites = (new ClipModel())->getEpisodeSoundbites($this->getPodcast()->id, $this->id);
|
||||
$this->soundbites = new ClipModel()
|
||||
->getEpisodeSoundbites($this->getPodcast()->id, $this->id);
|
||||
}
|
||||
|
||||
return $this->soundbites;
|
||||
|
|
@ -432,7 +426,8 @@ class Episode extends Entity
|
|||
public function getPosts(): array
|
||||
{
|
||||
if ($this->posts === null) {
|
||||
$this->posts = (new PostModel())->getEpisodePosts($this->id);
|
||||
$this->posts = new PostModel()
|
||||
->getEpisodePosts($this->id);
|
||||
}
|
||||
|
||||
return $this->posts;
|
||||
|
|
@ -444,18 +439,14 @@ class Episode extends Entity
|
|||
public function getComments(): array
|
||||
{
|
||||
if ($this->comments === null) {
|
||||
$this->comments = (new EpisodeCommentModel())->getEpisodeComments($this->id);
|
||||
$this->comments = new EpisodeCommentModel()
|
||||
->getEpisodeComments($this->id);
|
||||
}
|
||||
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function getLink(): string
|
||||
{
|
||||
return url_to('episode', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
|
||||
}
|
||||
|
||||
public function getEmbedUrl(string $theme = null): string
|
||||
public function getEmbedUrl(?string $theme = null): string
|
||||
{
|
||||
return $theme
|
||||
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
|
||||
|
|
@ -464,14 +455,15 @@ class Episode extends Entity
|
|||
|
||||
public function setGuid(?string $guid = null): static
|
||||
{
|
||||
$this->attributes['guid'] = $guid ?? $this->getLink();
|
||||
$this->attributes['guid'] = $guid ?? $this->link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPodcast(): ?Podcast
|
||||
{
|
||||
return (new PodcastModel())->getPodcastById($this->podcast_id);
|
||||
return new PodcastModel()
|
||||
->getPodcastById($this->podcast_id);
|
||||
}
|
||||
|
||||
public function setDescriptionMarkdown(string $descriptionMarkdown): static
|
||||
|
|
@ -495,34 +487,6 @@ class Episode extends Entity
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getDescriptionHtml(?string $serviceSlug = null): string
|
||||
{
|
||||
$descriptionHtml = '';
|
||||
if (
|
||||
$this->getPodcast()
|
||||
->partner_id !== null &&
|
||||
$this->getPodcast()
|
||||
->partner_link_url !== null &&
|
||||
$this->getPodcast()
|
||||
->partner_image_url !== null
|
||||
) {
|
||||
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
|
||||
$serviceSlug,
|
||||
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
|
||||
$serviceSlug,
|
||||
)}\" alt=\"Partner image\" /></a></div>";
|
||||
}
|
||||
|
||||
$descriptionHtml .= $this->attributes['description_html'];
|
||||
|
||||
if ($this->getPodcast()->episode_description_footer_html) {
|
||||
$descriptionHtml .= "<footer>{$this->getPodcast()
|
||||
->episode_description_footer_html}</footer>";
|
||||
}
|
||||
|
||||
return $descriptionHtml;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
if ($this->description === null) {
|
||||
|
|
@ -591,96 +555,11 @@ class Episode extends Entity
|
|||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom rss tag as XML String
|
||||
*/
|
||||
public function getCustomRssString(): string
|
||||
{
|
||||
if ($this->custom_rss === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
helper('rss');
|
||||
|
||||
$xmlNode = (new SimpleRSSElement(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://podcastindex.org/namespace/1.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
|
||||
))
|
||||
->addChild('channel')
|
||||
->addChild('item');
|
||||
array_to_rss([
|
||||
'elements' => $this->custom_rss,
|
||||
], $xmlNode);
|
||||
|
||||
return str_replace(['<item>', '</item>'], '', (string) $xmlNode->asXML());
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves custom rss tag into json
|
||||
*/
|
||||
public function setCustomRssString(?string $customRssString = null): static
|
||||
{
|
||||
if ($customRssString === '') {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
helper('rss');
|
||||
|
||||
$customXML = simplexml_load_string(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://podcastindex.org/namespace/1.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
|
||||
$customRssString .
|
||||
'</item></channel></rss>',
|
||||
);
|
||||
|
||||
if (! $customXML instanceof SimpleXMLElement) {
|
||||
// TODO: Failed to parse custom xml, should return error?
|
||||
return $this;
|
||||
}
|
||||
|
||||
$customRssArray = rss_to_array($customXML)['elements'][0]['elements'][0];
|
||||
|
||||
if (array_key_exists('elements', $customRssArray)) {
|
||||
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
|
||||
} else {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPartnerLink(?string $serviceSlug = null): string
|
||||
{
|
||||
$partnerLink =
|
||||
rtrim((string) $this->getPodcast()->partner_link_url, '/') .
|
||||
'?pid=' .
|
||||
$this->getPodcast()
|
||||
->partner_id .
|
||||
'&guid=' .
|
||||
urlencode((string) $this->attributes['guid']);
|
||||
|
||||
if ($serviceSlug !== null) {
|
||||
$partnerLink .= '&_from=' . $serviceSlug;
|
||||
}
|
||||
|
||||
return $partnerLink;
|
||||
}
|
||||
|
||||
public function getPartnerImageUrl(string $serviceSlug = null): string
|
||||
{
|
||||
return rtrim((string) $this->getPodcast()->partner_image_url, '/') .
|
||||
'?pid=' .
|
||||
$this->getPodcast()
|
||||
->partner_id .
|
||||
'&guid=' .
|
||||
urlencode((string) $this->attributes['guid']) .
|
||||
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
|
||||
}
|
||||
|
||||
public function getPreviewLink(): string
|
||||
{
|
||||
if ($this->preview_id === null) {
|
||||
// generate preview id
|
||||
if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) {
|
||||
if (! $previewUUID = new EpisodeModel()->setEpisodePreviewId($this->id)) {
|
||||
throw new Exception('Could not set episode preview id');
|
||||
}
|
||||
|
||||
|
|
@ -695,6 +574,7 @@ class Episode extends Entity
|
|||
*/
|
||||
public function getClipCount(): int|string
|
||||
{
|
||||
return (new ClipModel())->getClipCount($this->podcast_id, $this->id);
|
||||
return new ClipModel()
|
||||
->getClipCount($this->podcast_id, $this->id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use RuntimeException;
|
|||
* @property Episode|null $episode
|
||||
* @property int $actor_id
|
||||
* @property Actor|null $actor
|
||||
* @property string|null $in_reply_to_id
|
||||
* @property ?string $in_reply_to_id
|
||||
* @property EpisodeComment|null $reply_to_comment
|
||||
* @property string $message
|
||||
* @property string $message_html
|
||||
|
|
@ -76,7 +76,8 @@ class EpisodeComment extends UuidEntity
|
|||
public function getEpisode(): ?Episode
|
||||
{
|
||||
if (! $this->episode instanceof Episode) {
|
||||
$this->episode = (new EpisodeModel())->getEpisodeById($this->episode_id);
|
||||
$this->episode = new EpisodeModel()
|
||||
->getEpisodeById($this->episode_id);
|
||||
}
|
||||
|
||||
return $this->episode;
|
||||
|
|
@ -100,9 +101,9 @@ class EpisodeComment extends UuidEntity
|
|||
*/
|
||||
public function getReplies(): array
|
||||
{
|
||||
|
||||
if ($this->replies === null) {
|
||||
$this->replies = (new EpisodeCommentModel())->getCommentReplies($this->id);
|
||||
$this->replies = new EpisodeCommentModel()
|
||||
->getCommentReplies($this->id);
|
||||
}
|
||||
|
||||
return $this->replies;
|
||||
|
|
|
|||
|
|
@ -22,15 +22,9 @@ use CodeIgniter\Entity\Entity;
|
|||
*/
|
||||
class Location extends Entity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const OSM_URL = 'https://www.openstreetmap.org/';
|
||||
private const string OSM_URL = 'https://www.openstreetmap.org/';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
|
||||
private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
|
||||
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
|
|
@ -108,14 +102,14 @@ class Location extends Entity
|
|||
|
||||
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
|
||||
$places[0],
|
||||
'lon'
|
||||
'lon',
|
||||
) && $places[0]->lon !== null)) {
|
||||
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
|
||||
}
|
||||
|
||||
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
|
||||
$places[0],
|
||||
'osm_id'
|
||||
'osm_id',
|
||||
) && $places[0]->osm_id !== null)) {
|
||||
$this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use RuntimeException;
|
|||
* @property string $full_name
|
||||
* @property string $unique_name
|
||||
* @property string|null $information_url
|
||||
* @property int|null $avatar_id
|
||||
* @property ?int $avatar_id
|
||||
* @property ?Image $avatar
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
|
|
@ -56,7 +56,7 @@ class Person extends Entity
|
|||
/**
|
||||
* Saves the person avatar in `public/media/persons/`
|
||||
*/
|
||||
public function setAvatar(UploadedFile | File $file = null): static
|
||||
public function setAvatar(UploadedFile | File|null $file = null): static
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -67,7 +67,8 @@ class Person extends Entity
|
|||
->setFile($file);
|
||||
$this->getAvatar()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('image'))->updateMedia($this->getAvatar());
|
||||
new MediaModel('image')
|
||||
->updateMedia($this->getAvatar());
|
||||
} else {
|
||||
$avatar = new Image([
|
||||
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
|
||||
|
|
@ -78,7 +79,7 @@ class Person extends Entity
|
|||
]);
|
||||
$avatar->setFile($file);
|
||||
|
||||
$this->attributes['avatar_id'] = (new MediaModel('image'))->saveMedia($avatar);
|
||||
$this->attributes['avatar_id'] = new MediaModel('image')->saveMedia($avatar);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -91,7 +92,8 @@ class Person extends Entity
|
|||
}
|
||||
|
||||
if (! $this->avatar instanceof Image) {
|
||||
$this->avatar = (new MediaModel('image'))->getMediaById($this->avatar_id);
|
||||
$this->avatar = new MediaModel('image')
|
||||
->getMediaById($this->avatar_id);
|
||||
}
|
||||
|
||||
return $this->avatar;
|
||||
|
|
@ -107,11 +109,12 @@ class Person extends Entity
|
|||
}
|
||||
|
||||
if ($this->roles === null) {
|
||||
$this->roles = (new PersonModel())->getPersonRoles(
|
||||
$this->id,
|
||||
(int) $this->attributes['podcast_id'],
|
||||
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null
|
||||
);
|
||||
$this->roles = new PersonModel()
|
||||
->getPersonRoles(
|
||||
$this->id,
|
||||
(int) $this->attributes['podcast_id'],
|
||||
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->roles;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Libraries\SimpleRSSElement;
|
||||
use App\Models\ActorModel;
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\EpisodeModel;
|
||||
|
|
@ -56,18 +55,14 @@ use RuntimeException;
|
|||
* @property string $language_code
|
||||
* @property int $category_id
|
||||
* @property Category|null $category
|
||||
* @property int[]|null $other_categories_ids
|
||||
* @property int[] $other_categories_ids
|
||||
* @property Category[] $other_categories
|
||||
* @property string|null $parental_advisory
|
||||
* @property string|null $publisher
|
||||
* @property string $owner_name
|
||||
* @property string $owner_email
|
||||
* @property bool $is_owner_email_removed_from_feed
|
||||
* @property string $type
|
||||
* @property string $medium
|
||||
* @property string|null $copyright
|
||||
* @property string|null $episode_description_footer_markdown
|
||||
* @property string|null $episode_description_footer_html
|
||||
* @property bool $is_blocked
|
||||
* @property bool $is_completed
|
||||
* @property bool $is_locked
|
||||
|
|
@ -77,15 +72,7 @@ use RuntimeException;
|
|||
* @property string|null $location_name
|
||||
* @property string|null $location_geo
|
||||
* @property string|null $location_osm
|
||||
* @property string|null $payment_pointer
|
||||
* @property array<string|int,mixed>|null $custom_rss
|
||||
* @property bool $is_op3_enabled
|
||||
* @property string $op3_url
|
||||
* @property string $custom_rss_string
|
||||
* @property bool $is_published_on_hubs
|
||||
* @property string|null $partner_id
|
||||
* @property string|null $partner_link_url
|
||||
* @property string|null $partner_image_url
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property string $publication_status
|
||||
|
|
@ -125,9 +112,9 @@ class Podcast extends Entity
|
|||
protected ?array $other_categories = null;
|
||||
|
||||
/**
|
||||
* @var int[]|null
|
||||
* @var int[]
|
||||
*/
|
||||
protected ?array $other_categories_ids = null;
|
||||
protected array $other_categories_ids = [];
|
||||
|
||||
/**
|
||||
* @var Episode[]|null
|
||||
|
|
@ -166,8 +153,6 @@ class Podcast extends Entity
|
|||
|
||||
protected ?Location $location = null;
|
||||
|
||||
protected string $custom_rss_string;
|
||||
|
||||
protected ?string $publication_status = null;
|
||||
|
||||
/**
|
||||
|
|
@ -180,44 +165,35 @@ class Podcast extends Entity
|
|||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'guid' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'handle' => 'string',
|
||||
'title' => 'string',
|
||||
'description_markdown' => 'string',
|
||||
'description_html' => 'string',
|
||||
'cover_id' => 'int',
|
||||
'banner_id' => '?int',
|
||||
'language_code' => 'string',
|
||||
'category_id' => 'integer',
|
||||
'parental_advisory' => '?string',
|
||||
'publisher' => '?string',
|
||||
'owner_name' => 'string',
|
||||
'owner_email' => 'string',
|
||||
'is_owner_email_removed_from_feed' => 'boolean',
|
||||
'type' => 'string',
|
||||
'medium' => 'string',
|
||||
'copyright' => '?string',
|
||||
'episode_description_footer_markdown' => '?string',
|
||||
'episode_description_footer_html' => '?string',
|
||||
'is_blocked' => 'boolean',
|
||||
'is_completed' => 'boolean',
|
||||
'is_locked' => 'boolean',
|
||||
'is_premium_by_default' => 'boolean',
|
||||
'imported_feed_url' => '?string',
|
||||
'new_feed_url' => '?string',
|
||||
'location_name' => '?string',
|
||||
'location_geo' => '?string',
|
||||
'location_osm' => '?string',
|
||||
'payment_pointer' => '?string',
|
||||
'custom_rss' => '?json-array',
|
||||
'is_published_on_hubs' => 'boolean',
|
||||
'partner_id' => '?string',
|
||||
'partner_link_url' => '?string',
|
||||
'partner_image_url' => '?string',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'id' => 'integer',
|
||||
'guid' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'handle' => 'string',
|
||||
'title' => 'string',
|
||||
'description_markdown' => 'string',
|
||||
'description_html' => 'string',
|
||||
'cover_id' => 'int',
|
||||
'banner_id' => '?int',
|
||||
'language_code' => 'string',
|
||||
'category_id' => 'integer',
|
||||
'parental_advisory' => '?string',
|
||||
'publisher' => '?string',
|
||||
'owner_name' => 'string',
|
||||
'owner_email' => 'string',
|
||||
'type' => 'string',
|
||||
'copyright' => '?string',
|
||||
'is_blocked' => 'boolean',
|
||||
'is_completed' => 'boolean',
|
||||
'is_locked' => 'boolean',
|
||||
'is_premium_by_default' => 'boolean',
|
||||
'imported_feed_url' => '?string',
|
||||
'new_feed_url' => '?string',
|
||||
'location_name' => '?string',
|
||||
'location_geo' => '?string',
|
||||
'location_osm' => '?string',
|
||||
'is_published_on_hubs' => 'boolean',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
public function getAtHandle(): string
|
||||
|
|
@ -239,7 +215,7 @@ class Podcast extends Entity
|
|||
return $this->actor;
|
||||
}
|
||||
|
||||
public function setCover(UploadedFile | File $file = null): self
|
||||
public function setCover(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -250,7 +226,8 @@ class Podcast extends Entity
|
|||
->setFile($file);
|
||||
$this->getCover()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('image'))->updateMedia($this->getCover());
|
||||
new MediaModel('image')
|
||||
->updateMedia($this->getCover());
|
||||
} else {
|
||||
$cover = new Image([
|
||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
|
||||
|
|
@ -261,7 +238,7 @@ class Podcast extends Entity
|
|||
]);
|
||||
$cover->setFile($file);
|
||||
|
||||
$this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
|
||||
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -270,7 +247,8 @@ class Podcast extends Entity
|
|||
public function getCover(): Image
|
||||
{
|
||||
if (! $this->cover instanceof Image) {
|
||||
$cover = (new MediaModel('image'))->getMediaById($this->cover_id);
|
||||
$cover = new MediaModel('image')
|
||||
->getMediaById($this->cover_id);
|
||||
|
||||
if (! $cover instanceof Image) {
|
||||
throw new Exception('Could not retrieve podcast cover.');
|
||||
|
|
@ -282,7 +260,7 @@ class Podcast extends Entity
|
|||
return $this->cover;
|
||||
}
|
||||
|
||||
public function setBanner(UploadedFile | File $file = null): self
|
||||
public function setBanner(UploadedFile | File|null $file = null): self
|
||||
{
|
||||
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||
return $this;
|
||||
|
|
@ -293,7 +271,8 @@ class Podcast extends Entity
|
|||
->setFile($file);
|
||||
$this->getBanner()
|
||||
->updated_by = $this->attributes['updated_by'];
|
||||
(new MediaModel('image'))->updateMedia($this->getBanner());
|
||||
new MediaModel('image')
|
||||
->updateMedia($this->getBanner());
|
||||
} else {
|
||||
$banner = new Image([
|
||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
|
||||
|
|
@ -304,7 +283,7 @@ class Podcast extends Entity
|
|||
]);
|
||||
$banner->setFile($file);
|
||||
|
||||
$this->attributes['banner_id'] = (new MediaModel('image'))->saveMedia($banner);
|
||||
$this->attributes['banner_id'] = new MediaModel('image')->saveMedia($banner);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
@ -317,7 +296,8 @@ class Podcast extends Entity
|
|||
}
|
||||
|
||||
if (! $this->banner instanceof Image) {
|
||||
$this->banner = (new MediaModel('image'))->getMediaById($this->banner_id);
|
||||
$this->banner = new MediaModel('image')
|
||||
->getMediaById($this->banner_id);
|
||||
}
|
||||
|
||||
return $this->banner;
|
||||
|
|
@ -341,7 +321,8 @@ class Podcast extends Entity
|
|||
public function getEpisodes(): array
|
||||
{
|
||||
if ($this->episodes === null) {
|
||||
$this->episodes = (new EpisodeModel())->getPodcastEpisodes($this->id, $this->type);
|
||||
$this->episodes = new EpisodeModel()
|
||||
->getPodcastEpisodes($this->id, $this->type);
|
||||
}
|
||||
|
||||
return $this->episodes;
|
||||
|
|
@ -352,7 +333,8 @@ class Podcast extends Entity
|
|||
*/
|
||||
public function getEpisodesCount(): int|string
|
||||
{
|
||||
return (new EpisodeModel())->getPodcastEpisodesCount($this->id);
|
||||
return new EpisodeModel()
|
||||
->getPodcastEpisodesCount($this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -363,7 +345,8 @@ class Podcast extends Entity
|
|||
public function getPersons(): array
|
||||
{
|
||||
if ($this->persons === null) {
|
||||
$this->persons = (new PersonModel())->getPodcastPersons($this->id);
|
||||
$this->persons = new PersonModel()
|
||||
->getPodcastPersons($this->id);
|
||||
}
|
||||
|
||||
return $this->persons;
|
||||
|
|
@ -375,7 +358,8 @@ class Podcast extends Entity
|
|||
public function getCategory(): ?Category
|
||||
{
|
||||
if (! $this->category instanceof Category) {
|
||||
$this->category = (new CategoryModel())->getCategoryById($this->category_id);
|
||||
$this->category = new CategoryModel()
|
||||
->getCategoryById($this->category_id);
|
||||
}
|
||||
|
||||
return $this->category;
|
||||
|
|
@ -389,7 +373,8 @@ class Podcast extends Entity
|
|||
public function getSubscriptions(): array
|
||||
{
|
||||
if ($this->subscriptions === null) {
|
||||
$this->subscriptions = (new SubscriptionModel())->getPodcastSubscriptions($this->id);
|
||||
$this->subscriptions = new SubscriptionModel()
|
||||
->getPodcastSubscriptions($this->id);
|
||||
}
|
||||
|
||||
return $this->subscriptions;
|
||||
|
|
@ -403,7 +388,8 @@ class Podcast extends Entity
|
|||
public function getContributors(): array
|
||||
{
|
||||
if ($this->contributors === null) {
|
||||
$this->contributors = (new UserModel())->getPodcastContributors($this->id);
|
||||
$this->contributors = new UserModel()
|
||||
->getPodcastContributors($this->id);
|
||||
}
|
||||
|
||||
return $this->contributors;
|
||||
|
|
@ -430,42 +416,6 @@ class Podcast extends Entity
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setEpisodeDescriptionFooterMarkdown(?string $episodeDescriptionFooterMarkdown = null): static
|
||||
{
|
||||
if ($episodeDescriptionFooterMarkdown === null || $episodeDescriptionFooterMarkdown === '') {
|
||||
$this->attributes[
|
||||
'episode_description_footer_markdown'
|
||||
] = null;
|
||||
$this->attributes[
|
||||
'episode_description_footer_html'
|
||||
] = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$config = [
|
||||
'html_input' => 'escape',
|
||||
'allow_unsafe_links' => false,
|
||||
];
|
||||
|
||||
$environment = new Environment($config);
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new AutolinkExtension());
|
||||
$environment->addExtension(new SmartPunctExtension());
|
||||
$environment->addExtension(new DisallowedRawHtmlExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
|
||||
$this->attributes[
|
||||
'episode_description_footer_markdown'
|
||||
] = $episodeDescriptionFooterMarkdown;
|
||||
$this->attributes[
|
||||
'episode_description_footer_html'
|
||||
] = $converter->convert($episodeDescriptionFooterMarkdown);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
if ($this->description === null) {
|
||||
|
|
@ -500,7 +450,8 @@ class Podcast extends Entity
|
|||
public function getPodcastingPlatforms(): array
|
||||
{
|
||||
if ($this->podcasting_platforms === null) {
|
||||
$this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
|
||||
$this->podcasting_platforms = new PlatformModel()
|
||||
->getPlatforms($this->id, 'podcasting');
|
||||
}
|
||||
|
||||
return $this->podcasting_platforms;
|
||||
|
|
@ -514,7 +465,8 @@ class Podcast extends Entity
|
|||
public function getSocialPlatforms(): array
|
||||
{
|
||||
if ($this->social_platforms === null) {
|
||||
$this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
|
||||
$this->social_platforms = new PlatformModel()
|
||||
->getPlatforms($this->id, 'social');
|
||||
}
|
||||
|
||||
return $this->social_platforms;
|
||||
|
|
@ -527,9 +479,9 @@ class Podcast extends Entity
|
|||
*/
|
||||
public function getFundingPlatforms(): array
|
||||
{
|
||||
|
||||
if ($this->funding_platforms === null) {
|
||||
$this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
|
||||
$this->funding_platforms = new PlatformModel()
|
||||
->getPlatforms($this->id, 'funding');
|
||||
}
|
||||
|
||||
return $this->funding_platforms;
|
||||
|
|
@ -541,18 +493,19 @@ class Podcast extends Entity
|
|||
public function getOtherCategories(): array
|
||||
{
|
||||
if ($this->other_categories === null) {
|
||||
$this->other_categories = (new CategoryModel())->getPodcastCategories($this->id);
|
||||
$this->other_categories = new CategoryModel()
|
||||
->getPodcastCategories($this->id);
|
||||
}
|
||||
|
||||
return $this->other_categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]|string[]
|
||||
* @return int[]
|
||||
*/
|
||||
public function getOtherCategoriesIds(): array
|
||||
{
|
||||
if ($this->other_categories_ids === null) {
|
||||
if ($this->other_categories_ids === []) {
|
||||
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
|
||||
}
|
||||
|
||||
|
|
@ -599,68 +552,10 @@ class Podcast extends Entity
|
|||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom rss tag as XML String
|
||||
*/
|
||||
public function getCustomRssString(): string
|
||||
{
|
||||
if ($this->attributes['custom_rss'] === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
helper('rss');
|
||||
|
||||
$xmlNode = (new SimpleRSSElement(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://podcastindex.org/namespace/1.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
|
||||
))->addChild('channel');
|
||||
array_to_rss([
|
||||
'elements' => $this->custom_rss,
|
||||
], $xmlNode);
|
||||
|
||||
return str_replace(['<channel>', '</channel>'], '', (string) $xmlNode->asXML());
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves custom rss tag into json
|
||||
*/
|
||||
public function setCustomRssString(string $customRssString): static
|
||||
{
|
||||
if ($customRssString === '') {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
helper('rss');
|
||||
$customRssArray = rss_to_array(
|
||||
simplexml_load_string(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://podcastindex.org/namespace/1.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel>' .
|
||||
$customRssString .
|
||||
'</channel></rss>',
|
||||
),
|
||||
)['elements'][0];
|
||||
|
||||
if (array_key_exists('elements', $customRssArray)) {
|
||||
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
|
||||
} else {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsPremium(): bool
|
||||
{
|
||||
// podcast is premium if at least one of its episodes is set as premium
|
||||
return (new EpisodeModel())->doesPodcastHavePremiumEpisodes($this->id);
|
||||
}
|
||||
|
||||
public function getIsOp3Enabled(): bool
|
||||
{
|
||||
return service('settings')->get('Analytics.enableOP3', 'podcast:' . $this->id);
|
||||
}
|
||||
|
||||
public function getOp3Url(): string
|
||||
{
|
||||
return 'https://op3.dev/show/' . $this->guid;
|
||||
return new EpisodeModel()
|
||||
->doesPodcastHavePremiumEpisodes($this->id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ class Post extends FediversePost
|
|||
}
|
||||
|
||||
if (! $this->episode instanceof Episode) {
|
||||
$this->episode = (new EpisodeModel())->getEpisodeById($this->episode_id);
|
||||
$this->episode = new EpisodeModel()
|
||||
->getEpisodeById($this->episode_id);
|
||||
}
|
||||
|
||||
return $this->episode;
|
||||
|
|
|
|||
|
|
@ -7,31 +7,38 @@ namespace App\Filters;
|
|||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Override;
|
||||
|
||||
class AllowCorsFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @param string[]|null $arguments
|
||||
* @param list<string>|null $arguments
|
||||
*
|
||||
* @return RequestInterface|ResponseInterface|string|null
|
||||
*/
|
||||
#[Override]
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|null $arguments
|
||||
* @return ResponseInterface|void
|
||||
* @param list<string>|null $arguments
|
||||
*
|
||||
* @return ResponseInterface|null
|
||||
*/
|
||||
#[Override]
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
if (! $response->hasHeader('Cache-Control')) {
|
||||
$response->setHeader('Cache-Control', 'public, max-age=86400');
|
||||
}
|
||||
|
||||
return $response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
|
||||
$response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
|
||||
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
|
||||
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
|
||||
->setHeader('Access-Control-Max-Age', '86400');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,6 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
if (! function_exists('render_breadcrumb')) {
|
||||
/**
|
||||
* Renders the breadcrumb navigation through the Breadcrumb service
|
||||
|
|
@ -15,20 +9,18 @@ if (! function_exists('render_breadcrumb')) {
|
|||
* @param string|null $class to be added to the breadcrumb nav
|
||||
* @return string html breadcrumb
|
||||
*/
|
||||
function render_breadcrumb(string $class = null): string
|
||||
function render_breadcrumb(?string $class = null): string
|
||||
{
|
||||
$breadcrumb = service('breadcrumb');
|
||||
return $breadcrumb->render($class);
|
||||
return service('breadcrumb')->render($class);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('replace_breadcrumb_params')) {
|
||||
/**
|
||||
* @param string[] $newParams
|
||||
* @param array<string|int,string> $newParams
|
||||
*/
|
||||
function replace_breadcrumb_params(array $newParams): void
|
||||
{
|
||||
$breadcrumb = service('breadcrumb');
|
||||
$breadcrumb->replaceParams(esc($newParams));
|
||||
service('breadcrumb')->replaceParams($newParams);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,31 +16,6 @@ use CodeIgniter\View\Table;
|
|||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('hint_tooltip')) {
|
||||
/**
|
||||
* Hint component
|
||||
*
|
||||
* Used to produce tooltip with a question mark icon for hint texts
|
||||
*
|
||||
* @param string $hintText The hint text
|
||||
*/
|
||||
function hint_tooltip(string $hintText = '', string $class = ''): string
|
||||
{
|
||||
$tooltip =
|
||||
'<span data-tooltip="bottom" tabindex="0" title="' .
|
||||
esc($hintText) .
|
||||
'" class="inline-block align-middle opacity-75 focus:ring-accent';
|
||||
|
||||
if ($class !== '') {
|
||||
$tooltip .= ' ' . $class;
|
||||
}
|
||||
|
||||
return $tooltip . '">' . icon('question-fill') . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('data_table')) {
|
||||
/**
|
||||
* Data table component
|
||||
|
|
@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) {
|
|||
*/
|
||||
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
||||
{
|
||||
$class = match ($publicationStatus) {
|
||||
'published' => 'text-pine-500 border-pine-500 bg-pine-50',
|
||||
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
|
||||
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50',
|
||||
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||
default => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||
$variant = match ($publicationStatus) {
|
||||
'published' => 'success',
|
||||
'scheduled' => 'warning',
|
||||
'with_podcast' => 'info',
|
||||
'not_published' => 'default',
|
||||
default => 'default',
|
||||
};
|
||||
|
||||
$title = match ($publicationStatus) {
|
||||
|
|
@ -130,16 +105,12 @@ if (! function_exists('publication_pill')) {
|
|||
|
||||
$label = lang('Episode.publication_status.' . $publicationStatus);
|
||||
|
||||
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' .
|
||||
$class .
|
||||
' ' .
|
||||
$customClass .
|
||||
'">' .
|
||||
$label .
|
||||
($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||
// @icon("error-warning-fill")
|
||||
return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
|
||||
'">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||
'class' => 'flex-shrink-0 ml-1 text-lg',
|
||||
]) : '') .
|
||||
'</span>';
|
||||
'</x-Pill>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +153,7 @@ if (! function_exists('publication_button')) {
|
|||
}
|
||||
|
||||
return <<<HTML
|
||||
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button>
|
||||
<x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +234,7 @@ if (! function_exists('episode_publication_status_banner')) {
|
|||
$bannerText = lang('Episode.publication_status_banner.text', [
|
||||
'publication_status' => $episode->publication_status,
|
||||
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
|
||||
$episode->published_at
|
||||
$episode->published_at,
|
||||
) : null,
|
||||
]);
|
||||
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
|
||||
|
|
@ -296,7 +267,7 @@ if (! function_exists('episode_numbering')) {
|
|||
?int $episodeNumber = null,
|
||||
?int $seasonNumber = null,
|
||||
string $class = '',
|
||||
bool $isAbbr = false
|
||||
bool $isAbbr = false,
|
||||
): string {
|
||||
if (! $episodeNumber && ! $seasonNumber) {
|
||||
return '';
|
||||
|
|
@ -356,7 +327,7 @@ if (! function_exists('location_link')) {
|
|||
'class' => 'mr-2 flex-shrink-0',
|
||||
]) . '<span class="truncate">' . esc($location->name) . '</span>',
|
||||
[
|
||||
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' .
|
||||
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' .
|
||||
($class === '' ? '' : " {$class}"),
|
||||
'target' => '_blank',
|
||||
'rel' => 'noreferrer noopener',
|
||||
|
|
@ -411,7 +382,7 @@ if (! function_exists('relative_time')) {
|
|||
function relative_time(Time $time, string $class = ''): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
'request',
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ATOM);
|
||||
|
|
@ -432,7 +403,7 @@ if (! function_exists('local_datetime')) {
|
|||
function local_datetime(Time $time): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
'request',
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ATOM);
|
||||
|
|
@ -461,7 +432,7 @@ if (! function_exists('local_date')) {
|
|||
function local_date(Time $time): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(service(
|
||||
'request'
|
||||
'request',
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
|
||||
|
|
|
|||
|
|
@ -23,20 +23,20 @@ if (! function_exists('form_textarea')) {
|
|||
// Unsets default rows and cols if defined in extra field as array or string.
|
||||
if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos(
|
||||
(string) preg_replace('~\s+~', '', $extra),
|
||||
'rows='
|
||||
'rows=',
|
||||
) !== false)) {
|
||||
unset($defaults['rows']);
|
||||
}
|
||||
|
||||
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
|
||||
(string) preg_replace('~\s+~', '', $extra),
|
||||
'cols='
|
||||
'cols=',
|
||||
) !== false)) {
|
||||
unset($defaults['cols']);
|
||||
}
|
||||
|
||||
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes(
|
||||
$extra
|
||||
$extra,
|
||||
) . '>' . $val . "</textarea>\n";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ if (! function_exists('get_podcast_banner')) {
|
|||
if (! $podcast->banner instanceof Image) {
|
||||
$defaultBanner = config('Images')
|
||||
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
|
||||
Images::class
|
||||
Images::class,
|
||||
)->podcastBannerDefaultPaths['default'];
|
||||
|
||||
$sizes = config('Images')
|
||||
|
|
@ -217,7 +217,7 @@ if (! function_exists('get_podcast_banner')) {
|
|||
|
||||
// return default site icon url
|
||||
return base_url(
|
||||
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null)
|
||||
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ if (! function_exists('get_podcast_banner_mimetype')) {
|
|||
|
||||
// return default site icon url
|
||||
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
|
||||
Images::class
|
||||
Images::class,
|
||||
)->podcastBannerDefaultMimeType;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,34 +16,35 @@ if (! function_exists('render_page_links')) {
|
|||
*
|
||||
* @return string html pages navigation
|
||||
*/
|
||||
function render_page_links(string $class = null, string $podcastHandle = null): string
|
||||
function render_page_links(?string $class = null, ?string $podcastHandle = null): string
|
||||
{
|
||||
$pages = (new PageModel())->findAll();
|
||||
$pages = new PageModel()
|
||||
->findAll();
|
||||
$links = anchor(route_to('home'), lang('Common.home'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
if ($podcastHandle !== null) {
|
||||
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
}
|
||||
|
||||
$links .= anchor(route_to('credits'), lang('Person.credits'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
$links .= anchor(route_to('map'), lang('Page.map.title'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
foreach ($pages as $page) {
|
||||
$links .= anchor($page->link, esc($page->title), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
}
|
||||
|
||||
// if set in .env, add legal notice link at the end of page links
|
||||
if (config('App')->legalNoticeURL !== null) {
|
||||
$links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ declare(strict_types=1);
|
|||
use App\Entities\Category;
|
||||
use App\Entities\Location;
|
||||
use App\Entities\Podcast;
|
||||
use App\Libraries\SimpleRSSElement;
|
||||
use App\Libraries\RssFeed;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Mimes;
|
||||
use Modules\Media\Entities\Chapters;
|
||||
use Modules\Media\Entities\Transcript;
|
||||
use Modules\Plugins\Core\Plugins;
|
||||
use Modules\PremiumPodcasts\Entities\Subscription;
|
||||
|
||||
if (! function_exists('get_rss_feed')) {
|
||||
|
|
@ -28,24 +29,21 @@ if (! function_exists('get_rss_feed')) {
|
|||
function get_rss_feed(
|
||||
Podcast $podcast,
|
||||
string $serviceSlug = '',
|
||||
Subscription $subscription = null,
|
||||
string $token = null
|
||||
?Subscription $subscription = null,
|
||||
?string $token = null,
|
||||
): string {
|
||||
/** @var Plugins $plugins */
|
||||
$plugins = service('plugins');
|
||||
|
||||
$episodes = $podcast->episodes;
|
||||
|
||||
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||
$rss = new RssFeed();
|
||||
|
||||
$podcastNamespace = 'https://podcastindex.org/namespace/1.0';
|
||||
|
||||
$atomNamespace = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
$rss = new SimpleRSSElement(
|
||||
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunesNamespace}' xmlns:podcast='{$podcastNamespace}' xmlns:atom='{$atomNamespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>"
|
||||
);
|
||||
$plugins->rssBeforeChannel($podcast);
|
||||
|
||||
$channel = $rss->addChild('channel');
|
||||
|
||||
$atomLink = $channel->addChild('link', null, $atomNamespace);
|
||||
$atomLink = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
|
||||
$atomLink->addAttribute('href', $podcast->feed_url);
|
||||
$atomLink->addAttribute('rel', 'self');
|
||||
$atomLink->addAttribute('type', 'application/rss+xml');
|
||||
|
|
@ -54,18 +52,18 @@ if (! function_exists('get_rss_feed')) {
|
|||
$websubHubs = config('WebSub')
|
||||
->hubs;
|
||||
foreach ($websubHubs as $websubHub) {
|
||||
$atomLinkHub = $channel->addChild('link', null, $atomNamespace);
|
||||
$atomLinkHub = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
|
||||
$atomLinkHub->addAttribute('href', $websubHub);
|
||||
$atomLinkHub->addAttribute('rel', 'hub');
|
||||
$atomLinkHub->addAttribute('type', 'application/rss+xml');
|
||||
}
|
||||
|
||||
if ($podcast->new_feed_url !== null) {
|
||||
$channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace);
|
||||
$channel->addChild('new-feed-url', $podcast->new_feed_url, RssFeed::ITUNES_NAMESPACE);
|
||||
}
|
||||
|
||||
// the last build date corresponds to the creation of the feed.xml cache
|
||||
$channel->addChild('lastBuildDate', (new Time('now'))->format(DATE_RFC1123));
|
||||
$channel->addChild('lastBuildDate', new Time('now')->format(DATE_RFC1123));
|
||||
$channel->addChild('generator', 'Castopod - https://castopod.org/');
|
||||
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
|
||||
|
||||
|
|
@ -76,22 +74,21 @@ if (! function_exists('get_rss_feed')) {
|
|||
$podcast->guid = $uuid->uuid5('ead4c236-bf58-58c6-a2c6-a6b28d128cb6', $podcast->feed_url)
|
||||
->toString();
|
||||
|
||||
(new PodcastModel())->save($podcast);
|
||||
new PodcastModel()
|
||||
->save($podcast);
|
||||
}
|
||||
|
||||
$channel->addChild('guid', $podcast->guid, $podcastNamespace);
|
||||
$channel->addChild('guid', $podcast->guid, RssFeed::PODCAST_NAMESPACE);
|
||||
$channel->addChild('title', $podcast->title, null, false);
|
||||
$channel->addChildWithCDATA('description', $podcast->description_html);
|
||||
|
||||
$channel->addChild('medium', $podcast->medium, $podcastNamespace);
|
||||
|
||||
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
|
||||
$itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
|
||||
|
||||
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
|
||||
|
||||
$channel->addChild('language', $podcast->language_code);
|
||||
if ($podcast->location instanceof Location) {
|
||||
$locationElement = $channel->addChild('location', $podcast->location->name, $podcastNamespace);
|
||||
$locationElement = $channel->addChild('location', $podcast->location->name, RssFeed::PODCAST_NAMESPACE);
|
||||
if ($podcast->location->geo !== null) {
|
||||
$locationElement->addAttribute('geo', $podcast->location->geo);
|
||||
}
|
||||
|
|
@ -101,38 +98,16 @@ if (! function_exists('get_rss_feed')) {
|
|||
}
|
||||
}
|
||||
|
||||
if ($podcast->payment_pointer !== null) {
|
||||
$valueElement = $channel->addChild('value', null, $podcastNamespace);
|
||||
$valueElement->addAttribute('type', 'webmonetization');
|
||||
$valueElement->addAttribute('method', 'ILP');
|
||||
$recipientElement = $valueElement->addChild('valueRecipient', null, $podcastNamespace);
|
||||
$recipientElement->addAttribute('name', $podcast->owner_name);
|
||||
$recipientElement->addAttribute('type', 'paymentpointer');
|
||||
$recipientElement->addAttribute('address', $podcast->payment_pointer);
|
||||
$recipientElement->addAttribute('split', '100');
|
||||
}
|
||||
|
||||
if ($podcast->is_owner_email_removed_from_feed) {
|
||||
$channel
|
||||
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace);
|
||||
} else {
|
||||
$channel
|
||||
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace)
|
||||
->addAttribute('owner', $podcast->owner_email);
|
||||
}
|
||||
|
||||
if ($podcast->verify_txt !== null) {
|
||||
$channel
|
||||
->addChild('txt', $podcast->verify_txt, $podcastNamespace)
|
||||
->addAttribute('purpose', 'verify');
|
||||
}
|
||||
$channel
|
||||
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
|
||||
->addAttribute('owner', $podcast->owner_email);
|
||||
|
||||
if ($podcast->imported_feed_url !== null) {
|
||||
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace);
|
||||
$channel->addChild('previousUrl', $podcast->imported_feed_url, RssFeed::PODCAST_NAMESPACE);
|
||||
}
|
||||
|
||||
foreach ($podcast->podcasting_platforms as $podcastingPlatform) {
|
||||
$podcastingPlatformElement = $channel->addChild('id', null, $podcastNamespace);
|
||||
$podcastingPlatformElement = $channel->addChild('id', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);
|
||||
if ($podcastingPlatform->account_id !== null) {
|
||||
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);
|
||||
|
|
@ -141,7 +116,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
$podcastingPlatformElement->addAttribute('url', $podcastingPlatform->link_url);
|
||||
}
|
||||
|
||||
$castopodSocialElement = $channel->addChild('social', null, $podcastNamespace);
|
||||
$castopodSocialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$castopodSocialElement->addAttribute('priority', '1');
|
||||
$castopodSocialElement->addAttribute('platform', 'castopod');
|
||||
$castopodSocialElement->addAttribute('protocol', 'activitypub');
|
||||
|
|
@ -149,7 +124,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
|
||||
|
||||
foreach ($podcast->social_platforms as $socialPlatform) {
|
||||
$socialElement = $channel->addChild('social', null, $podcastNamespace);
|
||||
$socialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$socialElement->addAttribute('priority', '2');
|
||||
$socialElement->addAttribute('platform', $socialPlatform->slug);
|
||||
|
||||
|
|
@ -157,7 +132,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
if (in_array(
|
||||
$socialPlatform->slug,
|
||||
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
|
||||
true
|
||||
true,
|
||||
)) {
|
||||
$socialElement->addAttribute('protocol', 'activitypub');
|
||||
} else {
|
||||
|
|
@ -171,41 +146,41 @@ if (! function_exists('get_rss_feed')) {
|
|||
$socialElement->addAttribute('accountUrl', esc($socialPlatform->link_url));
|
||||
|
||||
if ($socialPlatform->slug === 'mastodon') {
|
||||
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, $podcastNamespace);
|
||||
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$socialSignUpelement->addAttribute('priority', '1');
|
||||
$socialSignUpelement->addAttribute(
|
||||
'homeUrl',
|
||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||
(string) $socialPlatform->link_url,
|
||||
PHP_URL_HOST
|
||||
) . '/public'
|
||||
PHP_URL_HOST,
|
||||
) . '/public',
|
||||
);
|
||||
$socialSignUpelement->addAttribute(
|
||||
'signUpUrl',
|
||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||
(string) $socialPlatform->link_url,
|
||||
PHP_URL_HOST
|
||||
) . '/auth/sign_up'
|
||||
PHP_URL_HOST,
|
||||
) . '/auth/sign_up',
|
||||
);
|
||||
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
|
||||
'socialSignUp',
|
||||
null,
|
||||
$podcastNamespace
|
||||
RssFeed::PODCAST_NAMESPACE,
|
||||
);
|
||||
$castopodSocialSignUpelement->addAttribute('priority', '1');
|
||||
$castopodSocialSignUpelement->addAttribute(
|
||||
'homeUrl',
|
||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||
(string) $socialPlatform->link_url,
|
||||
PHP_URL_HOST
|
||||
) . '/public'
|
||||
PHP_URL_HOST,
|
||||
) . '/public',
|
||||
);
|
||||
$castopodSocialSignUpelement->addAttribute(
|
||||
'signUpUrl',
|
||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||
(string) $socialPlatform->link_url,
|
||||
PHP_URL_HOST
|
||||
) . '/auth/sign_up'
|
||||
PHP_URL_HOST,
|
||||
) . '/auth/sign_up',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -214,7 +189,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
$fundingPlatformElement = $channel->addChild(
|
||||
'funding',
|
||||
$fundingPlatform->account_id,
|
||||
$podcastNamespace,
|
||||
RssFeed::PODCAST_NAMESPACE,
|
||||
);
|
||||
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
|
||||
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);
|
||||
|
|
@ -222,9 +197,9 @@ if (! function_exists('get_rss_feed')) {
|
|||
|
||||
foreach ($podcast->persons as $person) {
|
||||
foreach ($person->roles as $role) {
|
||||
$personElement = $channel->addChild('person', $person->full_name, $podcastNamespace);
|
||||
$personElement = $channel->addChild('person', $person->full_name, RssFeed::PODCAST_NAMESPACE);
|
||||
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'federation'));
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
|
||||
|
||||
if ($person->information_url !== null) {
|
||||
$personElement->addAttribute('href', $person->information_url);
|
||||
|
|
@ -251,29 +226,26 @@ if (! function_exists('get_rss_feed')) {
|
|||
$channel->addChild(
|
||||
'explicit',
|
||||
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
|
||||
$itunesNamespace,
|
||||
RssFeed::ITUNES_NAMESPACE,
|
||||
);
|
||||
|
||||
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, $itunesNamespace, false);
|
||||
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
|
||||
$channel->addChild('link', $podcast->link);
|
||||
|
||||
$owner = $channel->addChild('owner', null, $itunesNamespace);
|
||||
$owner = $channel->addChild('owner', null, RssFeed::ITUNES_NAMESPACE);
|
||||
|
||||
$owner->addChild('name', $podcast->owner_name, $itunesNamespace, false);
|
||||
$owner->addChild('name', $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
|
||||
$owner->addChild('email', $podcast->owner_email, RssFeed::ITUNES_NAMESPACE);
|
||||
|
||||
if (! $podcast->is_owner_email_removed_from_feed) {
|
||||
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);
|
||||
}
|
||||
|
||||
$channel->addChild('type', $podcast->type, $itunesNamespace);
|
||||
$channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
|
||||
$podcast->copyright &&
|
||||
$channel->addChild('copyright', $podcast->copyright);
|
||||
if ($podcast->is_blocked || $subscription instanceof Subscription) {
|
||||
$channel->addChild('block', 'Yes', $itunesNamespace);
|
||||
$channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||
}
|
||||
|
||||
if ($podcast->is_completed) {
|
||||
$channel->addChild('complete', 'Yes', $itunesNamespace);
|
||||
$channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||
}
|
||||
|
||||
$image = $channel->addChild('image');
|
||||
|
|
@ -281,17 +253,16 @@ if (! function_exists('get_rss_feed')) {
|
|||
$image->addChild('title', $podcast->title, null, false);
|
||||
$image->addChild('link', $podcast->link);
|
||||
|
||||
if ($podcast->custom_rss !== null) {
|
||||
array_to_rss([
|
||||
'elements' => $podcast->custom_rss,
|
||||
], $channel);
|
||||
}
|
||||
// run plugins hook at the end
|
||||
$plugins->rssAfterChannel($podcast, $channel);
|
||||
|
||||
foreach ($episodes as $episode) {
|
||||
if ($episode->is_premium && ! $subscription instanceof Subscription) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugins->rssBeforeItem($episode);
|
||||
|
||||
$item = $channel->addChild('item');
|
||||
$item->addChild('title', $episode->title, null, false);
|
||||
$enclosure = $item->addChild('enclosure');
|
||||
|
|
@ -311,7 +282,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
$item->addChild('guid', $episode->guid);
|
||||
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
|
||||
if ($episode->location instanceof Location) {
|
||||
$locationElement = $item->addChild('location', $episode->location->name, $podcastNamespace);
|
||||
$locationElement = $item->addChild('location', $episode->location->name, RssFeed::PODCAST_NAMESPACE);
|
||||
if ($episode->location->geo !== null) {
|
||||
$locationElement->addAttribute('geo', $episode->location->geo);
|
||||
}
|
||||
|
|
@ -321,10 +292,10 @@ if (! function_exists('get_rss_feed')) {
|
|||
}
|
||||
}
|
||||
|
||||
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug));
|
||||
$item->addChild('duration', (string) round($episode->audio->duration), $itunesNamespace);
|
||||
$item->addChildWithCDATA('description', $episode->description_html);
|
||||
$item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
|
||||
$item->addChild('link', $episode->link);
|
||||
$episodeItunesImage = $item->addChild('image', null, $itunesNamespace);
|
||||
$episodeItunesImage = $item->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
|
||||
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
|
||||
|
||||
$episode->parental_advisory &&
|
||||
|
|
@ -333,18 +304,18 @@ if (! function_exists('get_rss_feed')) {
|
|||
$episode->parental_advisory === 'explicit'
|
||||
? 'true'
|
||||
: 'false',
|
||||
$itunesNamespace,
|
||||
RssFeed::ITUNES_NAMESPACE,
|
||||
);
|
||||
|
||||
$episode->number &&
|
||||
$item->addChild('episode', (string) $episode->number, $itunesNamespace);
|
||||
$item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
|
||||
$episode->season_number &&
|
||||
$item->addChild('season', (string) $episode->season_number, $itunesNamespace);
|
||||
$item->addChild('episodeType', $episode->type, $itunesNamespace);
|
||||
$item->addChild('season', (string) $episode->season_number, RssFeed::ITUNES_NAMESPACE);
|
||||
$item->addChild('episodeType', $episode->type, RssFeed::ITUNES_NAMESPACE);
|
||||
|
||||
// If episode is of type trailer, add podcast:trailer tag on channel level
|
||||
if ($episode->type === 'trailer') {
|
||||
$trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace);
|
||||
$trailer = $channel->addChild('trailer', $episode->title, RssFeed::PODCAST_NAMESPACE);
|
||||
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
|
||||
$trailer->addAttribute(
|
||||
'url',
|
||||
|
|
@ -357,43 +328,37 @@ if (! function_exists('get_rss_feed')) {
|
|||
}
|
||||
}
|
||||
|
||||
// add podcast namespace tags for season and episode
|
||||
$episode->season_number &&
|
||||
$item->addChild('season', (string) $episode->season_number, $podcastNamespace);
|
||||
$episode->number &&
|
||||
$item->addChild('episode', (string) $episode->number, $podcastNamespace);
|
||||
|
||||
// add link to episode comments as podcast-activity format
|
||||
$comments = $item->addChild('comments', null, $podcastNamespace);
|
||||
$comments = $item->addChild('comments', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
|
||||
$comments->addAttribute('contentType', 'application/podcast-activity+json');
|
||||
|
||||
if ($episode->getPosts()) {
|
||||
$socialInteractUri = $episode->getPosts()[0]
|
||||
->uri;
|
||||
$socialInteractElement = $item->addChild('socialInteract', null, $podcastNamespace);
|
||||
$socialInteractElement = $item->addChild('socialInteract', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$socialInteractElement->addAttribute('uri', $socialInteractUri);
|
||||
$socialInteractElement->addAttribute('priority', '1');
|
||||
$socialInteractElement->addAttribute('platform', 'castopod');
|
||||
$socialInteractElement->addAttribute('protocol', 'activitypub');
|
||||
$socialInteractElement->addAttribute(
|
||||
'accountId',
|
||||
"@{$podcast->actor->username}@{$podcast->actor->domain}"
|
||||
"@{$podcast->actor->username}@{$podcast->actor->domain}",
|
||||
);
|
||||
$socialInteractElement->addAttribute(
|
||||
'pubDate',
|
||||
$episode->getPosts()[0]
|
||||
->published_at->format(DateTime::ISO8601)
|
||||
->published_at->format(DateTime::ISO8601),
|
||||
);
|
||||
}
|
||||
|
||||
if ($episode->transcript instanceof Transcript) {
|
||||
$transcriptElement = $item->addChild('transcript', null, $podcastNamespace);
|
||||
$transcriptElement = $item->addChild('transcript', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$transcriptElement->addAttribute('url', $episode->transcript->file_url);
|
||||
$transcriptElement->addAttribute(
|
||||
'type',
|
||||
Mimes::guessTypeFromExtension(
|
||||
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION)
|
||||
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION),
|
||||
) ?? 'text/html',
|
||||
);
|
||||
// Castopod only allows for captions (SubRip files)
|
||||
|
|
@ -403,21 +368,21 @@ if (! function_exists('get_rss_feed')) {
|
|||
}
|
||||
|
||||
if ($episode->getChapters() instanceof Chapters) {
|
||||
$chaptersElement = $item->addChild('chapters', null, $podcastNamespace);
|
||||
$chaptersElement = $item->addChild('chapters', null, RssFeed::PODCAST_NAMESPACE);
|
||||
$chaptersElement->addAttribute('url', $episode->chapters->file_url);
|
||||
$chaptersElement->addAttribute('type', 'application/json+chapters');
|
||||
}
|
||||
|
||||
foreach ($episode->soundbites as $soundbite) {
|
||||
// TODO: differentiate video from soundbites?
|
||||
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, $podcastNamespace);
|
||||
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, RssFeed::PODCAST_NAMESPACE);
|
||||
$soundbiteElement->addAttribute('startTime', (string) $soundbite->start_time);
|
||||
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
|
||||
}
|
||||
|
||||
foreach ($episode->persons as $person) {
|
||||
foreach ($person->roles as $role) {
|
||||
$personElement = $item->addChild('person', esc($person->full_name), $podcastNamespace);
|
||||
$personElement = $item->addChild('person', esc($person->full_name), RssFeed::PODCAST_NAMESPACE);
|
||||
|
||||
$personElement->addAttribute(
|
||||
'role',
|
||||
|
|
@ -429,7 +394,7 @@ if (! function_exists('get_rss_feed')) {
|
|||
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
|
||||
);
|
||||
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'federation'));
|
||||
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
|
||||
|
||||
if ($person->information_url !== null) {
|
||||
$personElement->addAttribute('href', $person->information_url);
|
||||
|
|
@ -438,14 +403,10 @@ if (! function_exists('get_rss_feed')) {
|
|||
}
|
||||
|
||||
if ($episode->is_blocked) {
|
||||
$item->addChild('block', 'Yes', $itunesNamespace);
|
||||
$item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||
}
|
||||
|
||||
if ($episode->custom_rss !== null) {
|
||||
array_to_rss([
|
||||
'elements' => $episode->custom_rss,
|
||||
], $item);
|
||||
}
|
||||
$plugins->rssAfterItem($episode, $item);
|
||||
}
|
||||
|
||||
return $rss->asXML();
|
||||
|
|
@ -456,11 +417,9 @@ if (! function_exists('add_category_tag')) {
|
|||
/**
|
||||
* Adds <itunes:category> and <category> tags to node for a given category
|
||||
*/
|
||||
function add_category_tag(SimpleXMLElement $node, Category $category): void
|
||||
function add_category_tag(RssFeed $node, Category $category): void
|
||||
{
|
||||
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||
|
||||
$itunesCategory = $node->addChild('category', null, $itunesNamespace);
|
||||
$itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
|
||||
$itunesCategory->addAttribute(
|
||||
'text',
|
||||
$category->parent instanceof Category
|
||||
|
|
@ -469,7 +428,7 @@ if (! function_exists('add_category_tag')) {
|
|||
);
|
||||
|
||||
if ($category->parent instanceof Category) {
|
||||
$itunesCategoryChild = $itunesCategory->addChild('category', null, $itunesNamespace);
|
||||
$itunesCategoryChild = $itunesCategory->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
|
||||
$itunesCategoryChild->addAttribute('text', $category->apple_category);
|
||||
$node->addChild('category', $category->parent->apple_category);
|
||||
}
|
||||
|
|
@ -477,70 +436,3 @@ if (! function_exists('add_category_tag')) {
|
|||
$node->addChild('category', $category->apple_category);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('rss_to_array')) {
|
||||
/**
|
||||
* Converts XML to array
|
||||
*
|
||||
* FIXME: param should be SimpleRSSElement
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function rss_to_array(SimpleXMLElement $rssNode): array
|
||||
{
|
||||
$nameSpaces = ['', 'http://www.itunes.com/dtds/podcast-1.0.dtd', 'https://podcastindex.org/namespace/1.0'];
|
||||
$arrayNode = [];
|
||||
$arrayNode['name'] = $rssNode->getName();
|
||||
$arrayNode['namespace'] = $rssNode->getNamespaces(false);
|
||||
foreach ($rssNode->attributes() as $key => $value) {
|
||||
$arrayNode['attributes'][$key] = (string) $value;
|
||||
}
|
||||
|
||||
$textcontent = trim((string) $rssNode);
|
||||
if ($textcontent !== '') {
|
||||
$arrayNode['content'] = $textcontent;
|
||||
}
|
||||
|
||||
foreach ($nameSpaces as $currentNameSpace) {
|
||||
foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {
|
||||
$arrayNode['elements'][] = rss_to_array($childXmlNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $arrayNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('array_to_rss')) {
|
||||
/**
|
||||
* Inserts array (converted to XML node) in XML node
|
||||
*
|
||||
* @param array<string, mixed> $arrayNode
|
||||
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
|
||||
*/
|
||||
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode): SimpleRSSElement
|
||||
{
|
||||
if (array_key_exists('elements', $arrayNode)) {
|
||||
foreach ($arrayNode['elements'] as $childArrayNode) {
|
||||
$childXmlNode = $xmlNode->addChild(
|
||||
$childArrayNode['name'],
|
||||
$childArrayNode['content'] ?? null,
|
||||
$childArrayNode['namespace'] === []
|
||||
? null
|
||||
: current($childArrayNode['namespace'])
|
||||
);
|
||||
if (array_key_exists('attributes', $childArrayNode)) {
|
||||
foreach (
|
||||
$childArrayNode['attributes'] as $attributeKey => $attributeValue
|
||||
) {
|
||||
$childXmlNode->addAttribute($attributeKey, $attributeValue);
|
||||
}
|
||||
}
|
||||
|
||||
array_to_rss($childArrayNode, $childXmlNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $xmlNode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ use App\Entities\EpisodeComment;
|
|||
use App\Entities\Page;
|
||||
use App\Entities\Podcast;
|
||||
use App\Entities\Post;
|
||||
use Melbahja\Seo\MetaTags;
|
||||
use App\Libraries\HtmlHead;
|
||||
use Melbahja\Seo\Schema;
|
||||
use Melbahja\Seo\Schema\Thing;
|
||||
use Modules\Fediverse\Entities\PreviewCard;
|
||||
|
||||
/**
|
||||
* @copyright 2021 Ad Aures
|
||||
* @copyright 2024 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
if (! function_exists('get_podcast_metatags')) {
|
||||
function get_podcast_metatags(Podcast $podcast, string $page): string
|
||||
if (! function_exists('set_podcast_metatags')) {
|
||||
function set_podcast_metatags(Podcast $podcast, string $page): void
|
||||
{
|
||||
$category = '';
|
||||
if ($podcast->category->parent_id !== null) {
|
||||
|
|
@ -47,14 +47,15 @@ if (! function_exists('get_podcast_metatags')) {
|
|||
'inLanguage' => $podcast->language_code,
|
||||
'genre' => $category,
|
||||
],
|
||||
type: 'PodcastSeries'
|
||||
)
|
||||
type: 'PodcastSeries',
|
||||
),
|
||||
);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$metatags
|
||||
->title($podcast->title . ' (@' . $podcast->handle . ') • ' . lang('Podcast.' . $page))
|
||||
$head
|
||||
->title(sprintf('%s (@%s) • %s', $podcast->title, $podcast->handle, lang('Podcast.' . $page)))
|
||||
->description(esc($podcast->description))
|
||||
->image((string) $podcast->cover->og_url)
|
||||
->canonical((string) current_url())
|
||||
|
|
@ -62,24 +63,18 @@ if (! function_exists('get_podcast_metatags')) {
|
|||
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
|
||||
->og('locale', $podcast->language_code)
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('podcast-activity', esc($podcast->handle)),
|
||||
]);
|
||||
|
||||
if ($podcast->payment_pointer) {
|
||||
$metatags->meta('monetization', $podcast->payment_pointer);
|
||||
}
|
||||
|
||||
return '<link type="application/rss+xml" rel="alternate" title="' . esc(
|
||||
$podcast->title
|
||||
) . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent('<link type="application/rss+xml" rel="alternate" title="' . esc(
|
||||
$podcast->title,
|
||||
) . '" href="' . $podcast->feed_url . '" />' . $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_episode_metatags')) {
|
||||
function get_episode_metatags(Episode $episode): string
|
||||
if (! function_exists('set_episode_metatags')) {
|
||||
function set_episode_metatags(Episode $episode): void
|
||||
{
|
||||
$schema = new Schema(
|
||||
new Thing(
|
||||
|
|
@ -88,30 +83,31 @@ if (! function_exists('get_episode_metatags')) {
|
|||
'name' => $episode->title,
|
||||
'image' => $episode->cover->feed_url,
|
||||
'description' => $episode->description,
|
||||
'datePublished' => $episode->published_at->format(DATE_ISO8601),
|
||||
'datePublished' => $episode->published_at->format(DATE_ATOM),
|
||||
'timeRequired' => iso8601_duration($episode->audio->duration),
|
||||
'duration' => iso8601_duration($episode->audio->duration),
|
||||
'associatedMedia' => new Thing(
|
||||
props: [
|
||||
'contentUrl' => $episode->audio_url,
|
||||
],
|
||||
type: 'MediaObject'
|
||||
type: 'MediaObject',
|
||||
),
|
||||
'partOfSeries' => new Thing(
|
||||
props: [
|
||||
'name' => $episode->podcast->title,
|
||||
'url' => $episode->podcast->link,
|
||||
],
|
||||
type: 'PodcastSeries'
|
||||
type: 'PodcastSeries',
|
||||
),
|
||||
],
|
||||
type: 'PodcastEpisode'
|
||||
)
|
||||
type: 'PodcastEpisode',
|
||||
),
|
||||
);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$metatags
|
||||
$head
|
||||
->title($episode->title)
|
||||
->description(esc($episode->description))
|
||||
->image((string) $episode->cover->og_url, 'player')
|
||||
|
|
@ -122,50 +118,45 @@ if (! function_exists('get_episode_metatags')) {
|
|||
->og('locale', $episode->podcast->language_code)
|
||||
->og('audio', $episode->audio_opengraph_url)
|
||||
->og('audio:type', $episode->audio->file_mimetype)
|
||||
->meta('article:published_time', $episode->published_at->format(DATE_ISO8601))
|
||||
->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601))
|
||||
->meta('article:published_time', $episode->published_at->format(DATE_ATOM))
|
||||
->meta('article:modified_time', $episode->updated_at->format(DATE_ATOM))
|
||||
->twitter('audio:partner', $episode->podcast->publisher ?? '')
|
||||
->twitter('audio:artist_name', esc($episode->podcast->owner_name))
|
||||
->twitter('player', $episode->getEmbedUrl('light'))
|
||||
->twitter('player:width', (string) config('Embed')->width)
|
||||
->twitter('player:height', (string) config('Embed')->height)
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('episode', $episode->podcast->handle, $episode->slug),
|
||||
]);
|
||||
|
||||
if ($episode->podcast->payment_pointer) {
|
||||
$metatags->meta('monetization', $episode->podcast->payment_pointer);
|
||||
}
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc(
|
||||
$episode->title
|
||||
) . ' oEmbed json" />' . PHP_EOL . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . PHP_EOL . $schema->__toString();
|
||||
'href' => $episode->link,
|
||||
])
|
||||
->appendRawContent('<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug),
|
||||
) . '" title="' . esc(
|
||||
$episode->title,
|
||||
) . ' oEmbed json" />' . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug),
|
||||
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_post_metatags')) {
|
||||
function get_post_metatags(Post $post): string
|
||||
if (! function_exists('set_post_metatags')) {
|
||||
function set_post_metatags(Post $post): void
|
||||
{
|
||||
$socialMediaPosting = new Thing(
|
||||
props: [
|
||||
'@id' => url_to('post', esc($post->actor->username), $post->id),
|
||||
'datePublished' => $post->published_at->format(DATE_ISO8601),
|
||||
'datePublished' => $post->published_at->format(DATE_ATOM),
|
||||
'author' => new Thing(
|
||||
props: [
|
||||
'name' => $post->actor->display_name,
|
||||
'url' => $post->actor->uri,
|
||||
],
|
||||
type: 'Person'
|
||||
type: 'Person',
|
||||
),
|
||||
'text' => $post->message,
|
||||
],
|
||||
type: 'SocialMediaPosting'
|
||||
type: 'SocialMediaPosting',
|
||||
);
|
||||
|
||||
if ($post->episode_id !== null) {
|
||||
|
|
@ -177,10 +168,10 @@ if (! function_exists('get_post_metatags')) {
|
|||
props: [
|
||||
'name' => $post->episode->podcast->owner_name,
|
||||
],
|
||||
type: 'Person'
|
||||
type: 'Person',
|
||||
),
|
||||
],
|
||||
type: 'Audio'
|
||||
type: 'Audio',
|
||||
));
|
||||
} elseif ($post->preview_card instanceof PreviewCard) {
|
||||
$socialMediaPosting->__set('sharedContent', new Thing(
|
||||
|
|
@ -191,17 +182,19 @@ if (! function_exists('get_post_metatags')) {
|
|||
props: [
|
||||
'name' => $post->preview_card->author_name,
|
||||
],
|
||||
type: 'Person'
|
||||
type: 'Person',
|
||||
),
|
||||
],
|
||||
type: 'WebPage'
|
||||
type: 'WebPage',
|
||||
));
|
||||
}
|
||||
|
||||
$schema = new Schema($socialMediaPosting);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$head
|
||||
->title(lang('Post.title', [
|
||||
'actorDisplayName' => $post->actor->display_name,
|
||||
]))
|
||||
|
|
@ -209,18 +202,16 @@ if (! function_exists('get_post_metatags')) {
|
|||
->image($post->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('post', esc($post->actor->username), $post->id),
|
||||
]);
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent((string) $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_episode_comment_metatags')) {
|
||||
function get_episode_comment_metatags(EpisodeComment $episodeComment): string
|
||||
if (! function_exists('set_episode_comment_metatags')) {
|
||||
function set_episode_comment_metatags(EpisodeComment $episodeComment): void
|
||||
{
|
||||
$schema = new Schema(new Thing(
|
||||
props: [
|
||||
|
|
@ -228,24 +219,26 @@ if (! function_exists('get_episode_comment_metatags')) {
|
|||
'episode-comment',
|
||||
esc($episodeComment->actor->username),
|
||||
$episodeComment->episode->slug,
|
||||
$episodeComment->id
|
||||
$episodeComment->id,
|
||||
),
|
||||
'datePublished' => $episodeComment->created_at->format(DATE_ISO8601),
|
||||
'datePublished' => $episodeComment->created_at->format(DATE_ATOM),
|
||||
'author' => new Thing(
|
||||
props: [
|
||||
'name' => $episodeComment->actor->display_name,
|
||||
'url' => $episodeComment->actor->uri,
|
||||
],
|
||||
type: 'Person'
|
||||
type: 'Person',
|
||||
),
|
||||
'text' => $episodeComment->message,
|
||||
'upvoteCount' => $episodeComment->likes_count,
|
||||
],
|
||||
type: 'SocialMediaPosting'
|
||||
type: 'SocialMediaPosting',
|
||||
));
|
||||
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$head
|
||||
->title(lang('Comment.title', [
|
||||
'actorDisplayName' => $episodeComment->actor->display_name,
|
||||
'episodeTitle' => $episodeComment->episode->title,
|
||||
|
|
@ -254,26 +247,25 @@ if (! function_exists('get_episode_comment_metatags')) {
|
|||
->image($episodeComment->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to(
|
||||
'episode-comment',
|
||||
$episodeComment->actor->username,
|
||||
$episodeComment->episode->slug,
|
||||
$episodeComment->id
|
||||
$episodeComment->id,
|
||||
),
|
||||
]);
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent((string) $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_follow_metatags')) {
|
||||
function get_follow_metatags(Actor $actor): string
|
||||
if (! function_exists('set_follow_metatags')) {
|
||||
function set_follow_metatags(Actor $actor): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(lang('Podcast.followTitle', [
|
||||
'actorDisplayName' => $actor->display_name,
|
||||
]))
|
||||
|
|
@ -281,16 +273,15 @@ if (! function_exists('get_follow_metatags')) {
|
|||
->image($actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_remote_actions_metatags')) {
|
||||
function get_remote_actions_metatags(Post $post, string $action): string
|
||||
if (! function_exists('set_remote_actions_metatags')) {
|
||||
function set_remote_actions_metatags(Post $post, string $action): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(lang('Fediverse.' . $action . '.title', [
|
||||
'actorDisplayName' => $post->actor->display_name,
|
||||
],))
|
||||
|
|
@ -298,42 +289,40 @@ if (! function_exists('get_remote_actions_metatags')) {
|
|||
->image($post->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_home_metatags')) {
|
||||
function get_home_metatags(): string
|
||||
if (! function_exists('set_home_metatags')) {
|
||||
function set_home_metatags(): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(service('settings')->get('App.siteName'))
|
||||
->description(esc(service('settings')->get('App.siteDescription')))
|
||||
->image(get_site_icon_url('512'))
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_page_metatags')) {
|
||||
function get_page_metatags(Page $page): string
|
||||
if (! function_exists('set_page_metatags')) {
|
||||
function set_page_metatags(Page $page): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(
|
||||
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
|
||||
'settings'
|
||||
)->get('App.siteName')
|
||||
'settings',
|
||||
)->get('App.siteName'),
|
||||
)
|
||||
->description(esc(service('settings')->get('App.siteDescription')))
|
||||
->image(get_site_icon_url('512'))
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
+ br/***
|
||||
+ ca/***
|
||||
+ cs/***
|
||||
+ de/***
|
||||
+ en/***
|
||||
+ es/***
|
||||
+ fr/***
|
||||
+ lt/***
|
||||
+ nn-no/***
|
||||
+ pl/***
|
||||
+ de/***
|
||||
+ pt-br/***
|
||||
+ sr-latn/***
|
||||
+ nn-no/***
|
||||
+ es/***
|
||||
+ zh-hans/***
|
||||
+ ca/***
|
||||
+ br/***
|
||||
+ sr-latn/***
|
||||
- **
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => "Komentář {actorDisplayName} k {episodeTitle}",
|
||||
'back_to_comments' => 'Zpět na komentáře',
|
||||
'form' => [
|
||||
'episode_message_placeholder' => 'Napište komentář…',
|
||||
'reply_to_placeholder' => 'Odpovědět @{actorUsername}',
|
||||
'submit' => 'Odeslat',
|
||||
'submit_reply' => 'Odpovědět',
|
||||
],
|
||||
'likes' => '{numberOfLikes, plural,
|
||||
one {# se líbí}
|
||||
other {# se líbí}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# odpověď}
|
||||
other {# odpovědi}
|
||||
}',
|
||||
'like' => 'Líbí se mi',
|
||||
'reply' => 'Odpovědět',
|
||||
'view_replies' => 'Zobrazit odpovědi ({numberOfReplies})',
|
||||
'block_actor' => 'Blokovat uživatele @{actorUsername}',
|
||||
'block_domain' => 'Blokovat doménu @{actorDomain}',
|
||||
'delete' => 'Odstranit komentář',
|
||||
];
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'yes' => 'Ano',
|
||||
'no' => 'Ne',
|
||||
'cancel' => 'Zrušit',
|
||||
'optional' => 'Volitelné',
|
||||
'close' => 'Zavřít',
|
||||
'home' => 'Domů',
|
||||
'explicit' => 'Explicitní',
|
||||
'powered_by' => 'Běží na {castopod}',
|
||||
'go_back' => 'Jít zpět',
|
||||
'play_episode_button' => [
|
||||
'play' => 'Přehrát',
|
||||
'playing' => 'Přehrávání',
|
||||
],
|
||||
'read_more' => 'Číst více',
|
||||
'read_less' => 'Číst méně',
|
||||
'see_more' => 'Zobraz více',
|
||||
'see_less' => 'Zobrazit méně',
|
||||
'legal_notice' => 'Právní ustanovení',
|
||||
];
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'season' => 'Série: {{seasonNumber}}',
|
||||
'season_abbr' => 'S{seasonNumber}',
|
||||
'number' => 'Epizoda {episodeNumber}',
|
||||
'number_abbr' => 'Ep. {episodeNumber}',
|
||||
'season_episode' => 'Série {seasonNumber} epizoda {episodeNumber}',
|
||||
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
|
||||
'persons' => '{personsCount, plural,
|
||||
one {# osoba}
|
||||
other {# osoby}
|
||||
}',
|
||||
'persons_list' => 'Osoby',
|
||||
'back_to_episodes' => 'Zpět na epizody {podcast}',
|
||||
'comments' => 'Komentáře',
|
||||
'activity' => 'Aktivita',
|
||||
'chapters' => 'Kapitoly',
|
||||
'transcript' => 'Přepis',
|
||||
'description' => 'Popis epizody',
|
||||
'number_of_comments' => '{numberOfComments, plural,
|
||||
one {# komentář}
|
||||
other {# komentáře}
|
||||
}',
|
||||
'all_podcast_episodes' => 'Všechny epizody podcastu',
|
||||
'back_to_podcast' => 'Přejít zpět na podcast',
|
||||
'preview' => [
|
||||
'title' => 'Náhled',
|
||||
'not_published' => 'Nezveřejněno',
|
||||
'text' => '{publication_status, select,
|
||||
published {Tato epizoda ještě není publikována.}
|
||||
scheduled {Tato epizoda je naplánována na {publication_date}}
|
||||
with_podcast {Tato epizoda bude zveřejněna současně s podcastem.}
|
||||
other {Tato epizoda ještě není publikována.}
|
||||
}',
|
||||
'publish' => 'Publikovat',
|
||||
'publish_edit' => 'Editovat publikaci',
|
||||
],
|
||||
'no_chapters' => 'Pro tuto epizodu nejsou k dispozici žádné kapitoly.',
|
||||
'download_transcript' => 'Stáhnout přepis ({extension})',
|
||||
'no_transcript' => 'Pro tuto epizodu není k dispozici žádný přepis.',
|
||||
];
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2021 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'your_handle' => 'Vaše handle',
|
||||
'your_handle_hint' => 'Zadejte @username@doména, ze které chcete působit.',
|
||||
'follow' => [
|
||||
'label' => 'Sledovat',
|
||||
'title' => 'Sledovat {actorDisplayName}',
|
||||
'subtitle' => 'Budete sledovat:',
|
||||
'accountNotFound' => 'Účet nebyl nalezen.',
|
||||
'remoteFollowNotAllowed' => 'Zdá se, že server účtu neumožňuje vzdálené sledování…',
|
||||
'submit' => 'Pokračovat a sledovat',
|
||||
],
|
||||
'favourite' => [
|
||||
'title' => "Oblíbený příspěvek {actorDisplayName}",
|
||||
'subtitle' => 'Oblíbíte si:',
|
||||
'submit' => 'Pokračovat a oblíbit',
|
||||
],
|
||||
'reblog' => [
|
||||
'title' => "Sdílet {actorDisplayName}příspěvek",
|
||||
'subtitle' => 'Budete sdílet:',
|
||||
'submit' => 'Pokračovat ke sdílení',
|
||||
],
|
||||
'reply' => [
|
||||
'title' => "Odpovědět na příspěvek {actorDisplayName}",
|
||||
'subtitle' => 'Chystáte se odpovědět:',
|
||||
'submit' => 'Pokračovat k odpovědi',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'all_podcasts' => 'Všechny podcasty',
|
||||
'sort_by' => 'Seřadit podle',
|
||||
'sort_options' => [
|
||||
'activity' => 'Poslední aktivita',
|
||||
'created_desc' => 'Od nejnovějších',
|
||||
'created_asc' => 'Od nejstarších',
|
||||
],
|
||||
'no_podcast' => 'Nebyly nalezeny žádné podcasty',
|
||||
];
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'back_to_home' => 'Zpátky domů',
|
||||
'map' => [
|
||||
'title' => 'Mapa',
|
||||
'description' => 'Objevte epizody podcastu na {siteName} , které jsou umístěny na mapě! Cestujte přes mapu a poslouchejte epizody, které hovoří o konkrétních místech.',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'feed' => 'RSS Podcast kanál',
|
||||
'season' => 'Série: {seasonNumber}',
|
||||
'list_of_episodes_year' => '{year} epizody ({episodeCount})',
|
||||
'list_of_episodes_season' =>
|
||||
'Epizody ({episodeCount}) série {seasonNumber}',
|
||||
'no_episode' => 'Nebyla nalezena žádná epizoda',
|
||||
'follow' => 'Sledovat',
|
||||
'followTitle' => 'Sledujte {actorDisplayName} na fediverse!',
|
||||
'followers' => '{numberOfFollowers, plural,
|
||||
one {# sledující}
|
||||
other {# sledující}
|
||||
}',
|
||||
'posts' => '{numberOfPosts, plural,
|
||||
one {# příspěvek}
|
||||
other {# příspěvky}
|
||||
}',
|
||||
'links' => 'Odkazy',
|
||||
'activity' => 'Aktivita',
|
||||
'episodes' => 'Epizody',
|
||||
'episodes_title' => 'Epizody {podcastTitle}',
|
||||
'about' => 'Informace',
|
||||
'stats' => [
|
||||
'title' => 'Statistiky',
|
||||
'number_of_seasons' => '{0, plural,
|
||||
one {# série}
|
||||
other {# série}
|
||||
}',
|
||||
'number_of_episodes' => '{0, plural,
|
||||
one {# epizoda}
|
||||
other {# epizody}
|
||||
}',
|
||||
'first_published_at' => 'První epizoda zveřejněna na {0, date, medium}',
|
||||
],
|
||||
'sponsor' => 'Sponzorovat',
|
||||
'funding_links' => 'Odkazy na financování pro {podcastTitle}',
|
||||
'find_on' => 'Najít {podcastTitle} na',
|
||||
'listen_on' => 'Poslouchat na',
|
||||
'persons' => '{personsCount, plural,
|
||||
one {# osoba}
|
||||
other {# osoby}
|
||||
}',
|
||||
'persons_list' => 'Osoby',
|
||||
'castopod_website' => 'Castopod (webová stránka)',
|
||||
];
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => "Příspěvek {actorDisplayName}",
|
||||
'back_to_actor_posts' => 'Zpět na příspěvky {actor}',
|
||||
'actor_shared' => '{actor} sdílen(a)',
|
||||
'reply_to' => 'Odpovědět @{actorUsername}',
|
||||
'form' => [
|
||||
'message_placeholder' => 'Napsat zprávu…',
|
||||
'episode_message_placeholder' => 'Napsat zprávu pro epizodu…',
|
||||
'episode_url_placeholder' => 'URL epizody',
|
||||
'reply_to_placeholder' => 'Odpovědět @{actorUsername}',
|
||||
'submit' => 'Odeslat',
|
||||
'submit_reply' => 'Odpovědět',
|
||||
],
|
||||
'favourites' => '{numberOfFavourites, plural,
|
||||
one {# oblíbil}
|
||||
other {# oblíbili}
|
||||
}',
|
||||
'reblogs' => '{numberOfReblogs, plural,
|
||||
one {# sdílení}
|
||||
other {# sdílení}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# odpověď}
|
||||
other {# odpovědi}
|
||||
}',
|
||||
'expand' => 'Rozbalit příspěvek',
|
||||
'block_actor' => 'Blokovat uživatele @{actorUsername}',
|
||||
'block_domain' => 'Blokovat doménu @{actorDomain}',
|
||||
'delete' => 'Smazat příspěvek',
|
||||
];
|
||||
|
|
@ -9,7 +9,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
|
||||
return [
|
||||
'your_handle' => 'Your handle',
|
||||
'your_handle' => 'Your Fediverse handle',
|
||||
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
|
||||
'follow' => [
|
||||
'label' => 'Follow',
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ return [
|
|||
'no_episode' => 'No episode found!',
|
||||
'follow' => 'Follow',
|
||||
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
|
||||
'followers' => '{numberOfFollowers, plural,
|
||||
one {# follower}
|
||||
other {# followers}
|
||||
'fediverseFollowers' => '{numberOfFollowers, plural,
|
||||
one {# Fediverse follower}
|
||||
other {# Fediverse followers}
|
||||
}',
|
||||
'posts' => '{numberOfPosts, plural,
|
||||
one {# post}
|
||||
|
|
@ -42,7 +42,7 @@ return [
|
|||
}',
|
||||
'first_published_at' => 'First episode published on {0, date, medium}',
|
||||
],
|
||||
'sponsor' => 'Sponsor',
|
||||
'funding' => 'Funding',
|
||||
'funding_links' => 'Funding links for {podcastTitle}',
|
||||
'find_on' => 'Find {podcastTitle} on',
|
||||
'listen_on' => 'Listen on',
|
||||
|
|
@ -51,5 +51,5 @@ return [
|
|||
other {# persons}
|
||||
}',
|
||||
'persons_list' => 'Persons',
|
||||
'castopod_website' => 'Castopod (website)',
|
||||
'links_mainpage' => 'Podcast (main page)',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => "{actorDisplayName} pakomentavo „{episodeTitle}“",
|
||||
'back_to_comments' => 'Grįžti į komentarus',
|
||||
'form' => [
|
||||
'episode_message_placeholder' => 'Parašyti komentarą…',
|
||||
'reply_to_placeholder' => 'Atsakyti @{actorUsername}',
|
||||
'submit' => 'Siųsti',
|
||||
'submit_reply' => 'Atsakyti',
|
||||
],
|
||||
'likes' => '{numberOfLikes, plural,
|
||||
one {# patiktukas}
|
||||
few {# patiktukai}
|
||||
other {# patiktukų}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# atsakymas}
|
||||
few {# atsakymai}
|
||||
other {# atsakymų}
|
||||
}',
|
||||
'like' => 'Patinka',
|
||||
'reply' => 'Atsakyti',
|
||||
'view_replies' => 'Rodyti atsakymus ({numberOfReplies})',
|
||||
'block_actor' => 'Blokuoti naudotoją @{actorUsername}',
|
||||
'block_domain' => 'Blokuoti domeną @{actorDomain}',
|
||||
'delete' => 'Šalinti komentarą',
|
||||
];
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'yes' => 'Taip',
|
||||
'no' => 'Ne',
|
||||
'cancel' => 'Atsisakyti',
|
||||
'optional' => 'Neprivaloma',
|
||||
'close' => 'Užverti',
|
||||
'home' => 'Pradžia',
|
||||
'explicit' => 'Atviras',
|
||||
'powered_by' => 'Veikia {castopod} pagrindu',
|
||||
'go_back' => 'Grįžti',
|
||||
'play_episode_button' => [
|
||||
'play' => 'Leisti',
|
||||
'playing' => 'Leidžiama',
|
||||
],
|
||||
'read_more' => 'Išsamiau',
|
||||
'read_less' => 'Glausčiau',
|
||||
'see_more' => 'Išsamiau',
|
||||
'see_less' => 'Glausčiau',
|
||||
'legal_notice' => 'Teisinė informacija',
|
||||
];
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'season' => '{seasonNumber} sezonas',
|
||||
'season_abbr' => 'S{seasonNumber}',
|
||||
'number' => '{episodeNumber} epizodas',
|
||||
'number_abbr' => '{episodeNumber} ep.',
|
||||
'season_episode' => '{seasonNumber} sezono {episodeNumber} epizodas',
|
||||
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
|
||||
'persons' => '{personsCount, plural,
|
||||
one {# asmuo}
|
||||
few {# asmenys}
|
||||
other {# asmenų}
|
||||
}',
|
||||
'persons_list' => 'Asmenys',
|
||||
'back_to_episodes' => 'Grįžti į „{podcast}“ epizodų sąrašą',
|
||||
'comments' => 'Komentarai',
|
||||
'activity' => 'Veikla',
|
||||
'chapters' => 'Skyreliai',
|
||||
'transcript' => 'Nuorašas',
|
||||
'description' => 'Epizodo aprašymas',
|
||||
'number_of_comments' => '{numberOfComments, plural,
|
||||
one {# komentaras}
|
||||
few {# komentarai}
|
||||
other {# komentarų}
|
||||
}',
|
||||
'all_podcast_episodes' => 'Visi tinklalaidės epizodai',
|
||||
'back_to_podcast' => 'Grįžti į tinklalaidę',
|
||||
'preview' => [
|
||||
'title' => 'Peržiūrėti',
|
||||
'not_published' => 'Nepaskelbtas',
|
||||
'text' => '{publication_status, select,
|
||||
published {Šis epizodas dar nepaskelbtas.}
|
||||
scheduled {Šį epizodą planuojama paskelbti {publication_date}.}
|
||||
with_podcast {Šį epizodą planuojama paskelbti kartu su tinklalaide.}
|
||||
other {Šis epizodas dar nepaskelbtas.}
|
||||
}',
|
||||
'publish' => 'Paskelbti',
|
||||
'publish_edit' => 'Taisyti paskelbimą',
|
||||
],
|
||||
'no_chapters' => 'Šis epizodas neišskaidytas skyreliais.',
|
||||
'download_transcript' => 'Parsisiųsti nuorašą ({extension})',
|
||||
'no_transcript' => 'Šio epizodo nuorašas nepateiktas.',
|
||||
];
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2021 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'your_handle' => 'Jūsų paskyros vardas',
|
||||
'your_handle_hint' => 'Įrašykite naudotinos paskyros vardą @naudotojas@domenas formatu.',
|
||||
'follow' => [
|
||||
'label' => 'Sekti',
|
||||
'title' => 'Sekti {actorDisplayName}',
|
||||
'subtitle' => 'Ketinate sekti:',
|
||||
'accountNotFound' => 'Paskyra nerasta.',
|
||||
'remoteFollowNotAllowed' => 'Panašu, jog paskyros serveris neleidžia nuotolinių sekimo užklausų…',
|
||||
'submit' => 'Inicijuoti sekimą',
|
||||
],
|
||||
'favourite' => [
|
||||
'title' => "Pamėgti {actorDisplayName} įrašą",
|
||||
'subtitle' => 'Ketinate pamėgti:',
|
||||
'submit' => 'Inicijuoti pamėgimą',
|
||||
],
|
||||
'reblog' => [
|
||||
'title' => "Pasidalinti {actorDisplayName} įrašu",
|
||||
'subtitle' => 'Ketinate pasidalinti:',
|
||||
'submit' => 'Inicijuoti pasidalijimą',
|
||||
],
|
||||
'reply' => [
|
||||
'title' => "Atsakyti į {actorDisplayName} įrašą",
|
||||
'subtitle' => 'Ketinate atsakyti į:',
|
||||
'submit' => 'Inicijuoti atsakymą',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'all_podcasts' => 'Visos tinklalaidės',
|
||||
'sort_by' => 'Rikiuoti pagal',
|
||||
'sort_options' => [
|
||||
'activity' => 'Paskiausia veikla',
|
||||
'created_desc' => 'Prad',
|
||||
'created_asc' => 'Pirma seniausi',
|
||||
],
|
||||
'no_podcast' => 'Tinklalaidė nerasta',
|
||||
];
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'back_to_home' => 'Grįžti į pradžią',
|
||||
'map' => [
|
||||
'title' => 'Žemėlapis',
|
||||
'description' => 'Atraskite „{siteName}“ paskelbtus tinklalaidžių epizodus žemėlapyje! Keliaukite po žemėlapį ir klausykitės epizodų, kuriuose kalbama apie konkrečias vietoves.',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'feed' => 'Tinklalaidės RSS sklaidos kanalas',
|
||||
'season' => '{seasonNumber} sezonas',
|
||||
'list_of_episodes_year' => '{year} metų epizodai ({episodeCount})',
|
||||
'list_of_episodes_season' =>
|
||||
'{seasonNumber} sezono epizodai ({episodeCount})',
|
||||
'no_episode' => 'Epizodų nerasta!',
|
||||
'follow' => 'Sekti',
|
||||
'followTitle' => 'Sekti {actorDisplayName} Fedivisatoje!',
|
||||
'followers' => '{numberOfFollowers, plural,
|
||||
one {# sekėjas}
|
||||
few {# sekėjai}
|
||||
other {# sekėjų}
|
||||
}',
|
||||
'posts' => '{numberOfPosts, plural,
|
||||
one {# įrašas}
|
||||
few {# įrašai}
|
||||
other {# įrašų}
|
||||
}',
|
||||
'links' => 'Nuorodos',
|
||||
'activity' => 'Veikla',
|
||||
'episodes' => 'Epizodai',
|
||||
'episodes_title' => '„{podcastTitle}“ epizodai',
|
||||
'about' => 'Apie',
|
||||
'stats' => [
|
||||
'title' => 'Statistika',
|
||||
'number_of_seasons' => '{0, plural,
|
||||
one {# sezonas}
|
||||
few {# sezonai}
|
||||
other {# sezonų}
|
||||
}',
|
||||
'number_of_episodes' => '{0, plural,
|
||||
one {# epizodas}
|
||||
few {# epizodai}
|
||||
other {# epizodų}
|
||||
}',
|
||||
'first_published_at' => 'Pirmasis epizodas paskelbtas {0, date, medium}',
|
||||
],
|
||||
'sponsor' => 'Paremti',
|
||||
'funding_links' => '„{podcastTitle}“ rėmimo nuorodos',
|
||||
'find_on' => 'Raskite „{podcastTitle}“',
|
||||
'listen_on' => 'Klausykitės',
|
||||
'persons' => '{personsCount, plural,
|
||||
one {# asmuo}
|
||||
few {# asmenys}
|
||||
other {# asmenų}
|
||||
}',
|
||||
'persons_list' => 'Asmenys',
|
||||
'castopod_website' => 'Castopod (svetainė)',
|
||||
];
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => "{actorDisplayName} įrašas",
|
||||
'back_to_actor_posts' => 'Grįžti į {actor} įrašus',
|
||||
'actor_shared' => '{actor} pasidalijo',
|
||||
'reply_to' => 'Atsakyti @{actorUsername}',
|
||||
'form' => [
|
||||
'message_placeholder' => 'Parašykite žinutę…',
|
||||
'episode_message_placeholder' => 'Parašykite žinutę šiam epizodui…',
|
||||
'episode_url_placeholder' => 'Epizodo URL adresas',
|
||||
'reply_to_placeholder' => 'Atsakyti @{actorUsername}',
|
||||
'submit' => 'Siųsti',
|
||||
'submit_reply' => 'Atsakyti',
|
||||
],
|
||||
'favourites' => '{numberOfFavourites, plural,
|
||||
one {# pamėgimas}
|
||||
few {# pamėgimai}
|
||||
other {# pamėgimų}
|
||||
}',
|
||||
'reblogs' => '{numberOfReblogs, plural,
|
||||
one {# pasidalijimas}
|
||||
few {# pasidalijimai}
|
||||
other {# pasidalijimų}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# atsakymas}
|
||||
few {# atsakymai}
|
||||
other {# atsakymų}
|
||||
}',
|
||||
'expand' => 'Išskleisti įrašą',
|
||||
'block_actor' => 'Blokuoti naudotoją @{actorUsername}',
|
||||
'block_domain' => 'Blokuoti domeną @{actorDomain}',
|
||||
'delete' => 'Šalinti įrašą',
|
||||
];
|
||||
|
|
@ -32,12 +32,18 @@ class Breadcrumb
|
|||
$uri = '';
|
||||
foreach (current_url(true)->getSegments() as $segment) {
|
||||
$uri .= '/' . $segment;
|
||||
$this->links[] = [
|
||||
$link = [
|
||||
'text' => is_numeric($segment)
|
||||
? $segment
|
||||
: lang('Breadcrumb.' . $segment),
|
||||
'href' => base_url($uri),
|
||||
];
|
||||
|
||||
if (is_numeric($segment)) {
|
||||
$this->links[] = $link;
|
||||
} else {
|
||||
$this->links[$segment] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,20 +52,19 @@ class Breadcrumb
|
|||
*
|
||||
* Given a breadcrumb with numeric params, this function replaces them with the values provided in $newParams
|
||||
*
|
||||
* Example with `Home / podcasts / 1 / episodes / 1`
|
||||
* Example with `Home / podcasts / 1 / episodes / 1 / foo`
|
||||
*
|
||||
* $newParams = [ 0 => 'foo', 1 => 'bar' ] replaceParams($newParams);
|
||||
* $newParams = [ 0 => 'bar', 1 => 'baz', 'foo' => 'I Pity The Foo' ] replaceParams($newParams);
|
||||
*
|
||||
* The breadcrumb is now `Home / podcasts / foo / episodes / bar`
|
||||
* The breadcrumb is now `Home / podcasts / foo / episodes / bar / I Pity The Foo`
|
||||
*
|
||||
* @param string[] $newParams
|
||||
* @param array<string|int,string> $newParams
|
||||
*/
|
||||
public function replaceParams(array $newParams): void
|
||||
{
|
||||
foreach ($this->links as $key => $link) {
|
||||
if (is_numeric($link['text'])) {
|
||||
$this->links[$key]['text'] = $newParams[0];
|
||||
array_shift($newParams);
|
||||
foreach ($newParams as $key => $newValue) {
|
||||
if (array_key_exists($key, $this->links)) {
|
||||
$this->links[$key]['text'] = $newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +72,7 @@ class Breadcrumb
|
|||
/**
|
||||
* Renders the breadcrumb object as an accessible html breadcrumb nav
|
||||
*/
|
||||
public function render(string $class = null): string
|
||||
public function render(?string $class = null): string
|
||||
{
|
||||
$listItems = '';
|
||||
$keys = array_keys($this->links);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class CommentObject extends ObjectType
|
|||
'episode-comment-replies',
|
||||
esc($comment->actor->username),
|
||||
$comment->episode->slug,
|
||||
$comment->id
|
||||
$comment->id,
|
||||
);
|
||||
|
||||
$this->cc = [$comment->actor->followers_url];
|
||||
|
|
|
|||
188
app/Libraries/HtmlHead.php
Normal file
188
app/Libraries/HtmlHead.php
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Controllers\WebmanifestController;
|
||||
use Override;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/melbahja/seo
|
||||
*/
|
||||
class HtmlHead implements Stringable
|
||||
{
|
||||
protected ?string $title = null;
|
||||
|
||||
/**
|
||||
* @var array{name:string,value:string|null,attributes:array<string,string|null>}[]
|
||||
*/
|
||||
protected array $tags = [];
|
||||
|
||||
protected string $rawContent = '';
|
||||
|
||||
#[Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
helper('misc');
|
||||
$this
|
||||
->tag('meta', null, [
|
||||
'charset' => 'UTF-8',
|
||||
])
|
||||
->meta('viewport', 'width=device-width, initial-scale=1.0')
|
||||
->tag('link', null, [
|
||||
'rel' => 'icon',
|
||||
'type' => 'image/x-icon',
|
||||
'href' => get_site_icon_url('ico'),
|
||||
])
|
||||
->tag('link', null, [
|
||||
'rel' => 'apple-touch-icon',
|
||||
'href' => get_site_icon_url('180'),
|
||||
])
|
||||
->tag('link', null, [
|
||||
'rel' => 'manifest',
|
||||
// @phpstan-ignore-next-line
|
||||
'href' => isset($podcast) ? route_to('podcast-webmanifest', esc($podcast->handle)) : route_to(
|
||||
'webmanifest',
|
||||
),
|
||||
])
|
||||
->meta(
|
||||
'theme-color',
|
||||
WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
|
||||
)
|
||||
->tag('link', null, [
|
||||
'rel' => 'stylesheet',
|
||||
'type' => 'text/css',
|
||||
'href' => route_to('themes-colors-css'),
|
||||
])
|
||||
->appendRawContent(<<<HTML
|
||||
<script>
|
||||
// Check that service workers are supported
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Use the window load event to keep the page load performant
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/assets/sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
HTML);
|
||||
|
||||
if ($this->title) {
|
||||
$this->tag('title', esc($this->title));
|
||||
}
|
||||
|
||||
if (url_is(route_to('admin') . '*') || url_is(base_url(config('Auth')->gateway) . '*')) {
|
||||
// restricted admin and auth areas, do not index
|
||||
$this->meta('robots', 'noindex');
|
||||
} else {
|
||||
// public website, set siteHead hook only there
|
||||
service('plugins')
|
||||
->siteHead($this);
|
||||
}
|
||||
|
||||
$head = '<head>';
|
||||
foreach ($this->tags as $tag) {
|
||||
if ($tag['value'] === null) {
|
||||
$head .= <<<HTML
|
||||
<{$tag['name']}{$this->stringify_attributes($tag['attributes'])}/>
|
||||
HTML;
|
||||
} else {
|
||||
$head .= <<<HTML
|
||||
<{$tag['name']} {$this->stringify_attributes($tag['attributes'])}>{$tag['value']}</{$tag['name']}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
$head .= $this->rawContent . '</head>';
|
||||
|
||||
// reset head for next render
|
||||
$this->title = null;
|
||||
$this->tags = [];
|
||||
$this->rawContent = '';
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
public function title(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this->meta('title', $title)
|
||||
->og('title', $title)
|
||||
->twitter('title', $title);
|
||||
}
|
||||
|
||||
public function description(string $desc): self
|
||||
{
|
||||
return $this->meta('description', $desc)
|
||||
->og('description', $desc)
|
||||
->twitter('description', $desc);
|
||||
}
|
||||
|
||||
public function image(string $url, string $card = 'summary_large_image'): self
|
||||
{
|
||||
return $this->og('image', $url)
|
||||
->twitter('card', $card)
|
||||
->twitter('image', $url);
|
||||
}
|
||||
|
||||
public function canonical(string $url): self
|
||||
{
|
||||
return $this->tag('link', null, [
|
||||
'rel' => 'canonical',
|
||||
'href' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
public function twitter(string $name, string $value): self
|
||||
{
|
||||
$this->meta("twitter:{$name}", $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|null> $attributes
|
||||
*/
|
||||
public function tag(string $name, ?string $value = null, array $attributes = []): self
|
||||
{
|
||||
$this->tags[] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'attributes' => $attributes,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function meta(string $name, string $content): self
|
||||
{
|
||||
$this->tag('meta', null, [
|
||||
'name' => $name,
|
||||
'content' => $content,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function og(string $name, string $content): self
|
||||
{
|
||||
$this->meta('og:' . $name, $content);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendRawContent(string $content): self
|
||||
{
|
||||
$this->rawContent .= $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|null> $attributes
|
||||
*/
|
||||
private function stringify_attributes(array $attributes): string
|
||||
{
|
||||
return stringify_attributes($attributes);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,11 @@ declare(strict_types=1);
|
|||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\Router\Exceptions\RedirectException;
|
||||
use CodeIgniter\HTTP\Exceptions\RedirectException;
|
||||
use CodeIgniter\Router\Exceptions\RouterException;
|
||||
use CodeIgniter\Router\Router as CodeIgniterRouter;
|
||||
use Config\Routing;
|
||||
use Override;
|
||||
|
||||
class Router extends CodeIgniterRouter
|
||||
{
|
||||
|
|
@ -29,6 +31,7 @@ class Router extends CodeIgniterRouter
|
|||
*
|
||||
* @return boolean Whether the route was matched or not.
|
||||
*/
|
||||
#[Override]
|
||||
protected function checkRoutes(string $uri): bool
|
||||
{
|
||||
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
|
||||
|
|
@ -66,7 +69,7 @@ class Router extends CodeIgniterRouter
|
|||
|
||||
throw new RedirectException(
|
||||
preg_replace('#^' . $routeKey . '$#u', (string) $redirectTo, $uri),
|
||||
$this->collection->getRedirectCode($routeKey)
|
||||
$this->collection->getRedirectCode($routeKey),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +79,7 @@ class Router extends CodeIgniterRouter
|
|||
preg_match(
|
||||
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
|
||||
$uri,
|
||||
$matched
|
||||
$matched,
|
||||
);
|
||||
|
||||
if ($this->collection->shouldUseSupportedLocalesOnly()
|
||||
|
|
@ -179,24 +182,50 @@ class Router extends CodeIgniterRouter
|
|||
return true;
|
||||
}
|
||||
|
||||
[$controller] = explode('::', (string) $handler);
|
||||
if (str_contains((string) $handler, '::')) {
|
||||
[$controller, $methodAndParams] = explode('::', (string) $handler);
|
||||
} else {
|
||||
$controller = $handler;
|
||||
$methodAndParams = '';
|
||||
}
|
||||
|
||||
// Checks `/` in controller name
|
||||
if (str_contains($controller, '/')) {
|
||||
if (str_contains((string) $controller, '/')) {
|
||||
throw RouterException::forInvalidControllerName($handler);
|
||||
}
|
||||
|
||||
if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
|
||||
// Checks dynamic controller
|
||||
if (str_contains($controller, '$')) {
|
||||
if (str_contains((string) $controller, '$')) {
|
||||
throw RouterException::forDynamicController($handler);
|
||||
}
|
||||
|
||||
// Using back-references
|
||||
$handler = preg_replace('#^' . $routeKey . '$#u', (string) $handler, $uri);
|
||||
if (config(Routing::class)->multipleSegmentsOneParam === false) {
|
||||
// Using back-references
|
||||
$segments = explode(
|
||||
'/',
|
||||
(string) preg_replace('#\A' . $routeKey . '\z#u', (string) $handler, $uri),
|
||||
);
|
||||
} else {
|
||||
if (str_contains($methodAndParams, '/')) {
|
||||
[$method, $handlerParams] = explode('/', $methodAndParams, 2);
|
||||
$params = explode('/', $handlerParams);
|
||||
$handlerSegments = array_merge([$controller . '::' . $method], $params);
|
||||
} else {
|
||||
$handlerSegments = [$handler];
|
||||
}
|
||||
|
||||
$segments = [];
|
||||
|
||||
foreach ($handlerSegments as $segment) {
|
||||
$segments[] = $this->replaceBackReferences($segment, $matches);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$segments = explode('/', (string) $handler);
|
||||
}
|
||||
|
||||
$this->setRequest(explode('/', (string) $handler));
|
||||
$this->setRequest($segments);
|
||||
|
||||
$this->setMatchedRoute($matchedKey, $handler);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,34 @@ declare(strict_types=1);
|
|||
namespace App\Libraries;
|
||||
|
||||
use DOMDocument;
|
||||
use Override;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class SimpleRSSElement extends SimpleXMLElement
|
||||
class RssFeed extends SimpleXMLElement
|
||||
{
|
||||
public const ATOM_NS = 'atom';
|
||||
|
||||
public const ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
public const ITUNES_NS = 'itunes';
|
||||
|
||||
public const ITUNES_NAMESPACE = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||
|
||||
public const PODCAST_NS = 'podcast';
|
||||
|
||||
public const PODCAST_NAMESPACE = 'https://podcastindex.org/namespace/1.0';
|
||||
|
||||
public function __construct(string $contents = '')
|
||||
{
|
||||
parent::__construct(sprintf(
|
||||
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:atom='%s' xmlns:itunes='%s' xmlns:podcast='%s' xmlns:content='http://purl.org/rss/1.0/modules/content/'>%s</rss>",
|
||||
$this::ATOM_NAMESPACE,
|
||||
$this::ITUNES_NAMESPACE,
|
||||
$this::PODCAST_NAMESPACE,
|
||||
$contents,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child with $value inside CDATA
|
||||
*
|
||||
|
|
@ -47,6 +71,7 @@ class SimpleRSSElement extends SimpleXMLElement
|
|||
*
|
||||
* @return static The addChild method returns a SimpleXMLElement object representing the child added to the XML node.
|
||||
*/
|
||||
#[Override]
|
||||
public function addChild($name, $value = null, $namespace = null, $escape = true): static
|
||||
{
|
||||
$newChild = parent::addChild($name, null, $namespace);
|
||||
|
|
@ -61,4 +86,37 @@ class SimpleRSSElement extends SimpleXMLElement
|
|||
|
||||
return $newChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add RssFeed code into a RssFeed
|
||||
*
|
||||
* adapted from: https://stackoverflow.com/a/23527002
|
||||
*
|
||||
* @param self|array<self> $nodes
|
||||
*/
|
||||
public function appendNodes(self|array $nodes): void
|
||||
{
|
||||
if (! is_array($nodes)) {
|
||||
$nodes = [$nodes];
|
||||
}
|
||||
|
||||
foreach ($nodes as $element) {
|
||||
$namespaces = $element->getNamespaces();
|
||||
$namespace = array_first($namespaces) ?? null;
|
||||
|
||||
if (trim((string) $element) === '') {
|
||||
$simpleRSS = $this->addChild($element->getName(), null, $namespace);
|
||||
} else {
|
||||
$simpleRSS = $this->addChild($element->getName(), (string) $element, $namespace);
|
||||
}
|
||||
|
||||
foreach ($element->children() as $child) {
|
||||
$simpleRSS->appendNodes($child);
|
||||
}
|
||||
|
||||
foreach ($element->attributes() as $name => $value) {
|
||||
$simpleRSS->addAttribute($name, (string) $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,26 +4,32 @@ declare(strict_types=1);
|
|||
|
||||
namespace ViewComponents;
|
||||
|
||||
class Component implements ComponentInterface
|
||||
{
|
||||
protected string $slot = '';
|
||||
use Override;
|
||||
|
||||
protected string $class = '';
|
||||
abstract class Component implements ComponentInterface
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected array $props = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string|'boolean'|'array'|'number'>
|
||||
*/
|
||||
protected array $casts = [];
|
||||
|
||||
protected ?string $slot = null;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $attributes = [
|
||||
'class' => '',
|
||||
];
|
||||
protected array $attributes = [];
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
helper('viewcomponents');
|
||||
|
||||
// overwrite default attributes if set
|
||||
$this->attributes = [...$this->attributes, ...$attributes];
|
||||
|
||||
|
|
@ -42,11 +48,42 @@ class Component implements ComponentInterface
|
|||
if (is_callable([$this, $method])) {
|
||||
$this->{$method}($value);
|
||||
} else {
|
||||
if (array_key_exists($name, $this->casts)) {
|
||||
$value = match ($this->casts[$name]) {
|
||||
'boolean' => $value === 'true',
|
||||
'number' => (int) $value,
|
||||
'array' => json_decode(htmlspecialchars_decode($value), true),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
$this->{$name} = $value;
|
||||
}
|
||||
|
||||
// remove from attributes
|
||||
if (in_array($name, $this->props, true)) {
|
||||
unset($this->attributes[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
}
|
||||
|
||||
public function mergeClass(string $class): void
|
||||
{
|
||||
if (! array_key_exists('class', $this->attributes)) {
|
||||
$this->attributes['class'] = $class;
|
||||
} else {
|
||||
$this->attributes['class'] .= ' ' . $class;
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringifiedAttributes(): string
|
||||
{
|
||||
return stringify_attributes($this->attributes);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
return static::class . ': RENDER METHOD NOT IMPLEMENTED';
|
||||
|
|
|
|||
|
|
@ -43,38 +43,38 @@ class ComponentRenderer
|
|||
private function renderSelfClosingTags(string $output): string
|
||||
{
|
||||
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
|
||||
// Should match any Component tags <Component />
|
||||
// Should match any Component tags <x-Component />
|
||||
$pattern = "/
|
||||
<
|
||||
\s*
|
||||
(?<name>[A-Z][A-Za-z0-9\.]*?)
|
||||
\s*
|
||||
\\s*
|
||||
x[-\\:](?<name>[\\w\\-\\:\\.]*)
|
||||
\\s*
|
||||
(?<attributes>
|
||||
(?:
|
||||
\s+
|
||||
\\s+
|
||||
(?:
|
||||
(?:
|
||||
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
|
||||
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
|
||||
)
|
||||
|
|
||||
(?:
|
||||
[\w\-:.@]+
|
||||
[\\w\\-:.@]+
|
||||
(
|
||||
=
|
||||
(?:
|
||||
\\\"[^\\\"]*\\\"
|
||||
|
|
||||
\'[^\']*\'
|
||||
\\'[^\\']*\\'
|
||||
|
|
||||
[^\'\\\"=<>]+
|
||||
[^\\'\\\"=<>]+
|
||||
)
|
||||
)?
|
||||
)
|
||||
)
|
||||
)*
|
||||
\s*
|
||||
\\s*
|
||||
)
|
||||
\/>
|
||||
\\/>
|
||||
/x";
|
||||
|
||||
/*
|
||||
|
|
@ -96,8 +96,9 @@ class ComponentRenderer
|
|||
|
||||
private function renderPairedTags(string $output): string
|
||||
{
|
||||
$pattern = '/<\s*(?<name>[A-Z][A-Za-z0-9\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*\1\s*>/uUsm';
|
||||
ini_set('pcre.backtrack_limit', '-1');
|
||||
// ini_set('pcre.backtrack_limit', '-1');
|
||||
$pattern = '/<\s*x[-\:](?<name>[\w\-\:\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*x-\1\s*>/uiUsm';
|
||||
|
||||
/*
|
||||
$matches[0] = full tags matched and all of its content
|
||||
$matches[name] = pascal cased tag name
|
||||
|
|
@ -167,8 +168,6 @@ class ComponentRenderer
|
|||
(
|
||||
\"[^\"]+\"
|
||||
|
|
||||
\'[^\']+\'
|
||||
|
|
||||
\\\'[^\\\']+\\\'
|
||||
|
|
||||
[^\s>]+
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ViewComponents;
|
||||
|
||||
use CodeIgniter\View\ViewDecoratorInterface;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Enables rendering of View Components into the views.
|
||||
|
|
@ -15,6 +16,7 @@ class Decorator implements ViewDecoratorInterface
|
|||
{
|
||||
private static ?ComponentRenderer $components = null;
|
||||
|
||||
#[Override]
|
||||
public static function decorate(string $html): string
|
||||
{
|
||||
$components = self::factory();
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (! function_exists('flatten_attributes')) {
|
||||
/**
|
||||
* Stringify attributes for use in HTML tags.
|
||||
*
|
||||
* Helper function used to convert a string, array, or object of attributes to a string.
|
||||
*
|
||||
* @param mixed $attributes string, array, object
|
||||
*/
|
||||
function flatten_attributes(mixed $attributes, bool $js = false): string
|
||||
{
|
||||
$atts = '';
|
||||
|
||||
if ($attributes === null) {
|
||||
return $atts;
|
||||
}
|
||||
|
||||
if (is_string($attributes)) {
|
||||
return ' ' . $attributes;
|
||||
}
|
||||
|
||||
$attributes = (array) $attributes;
|
||||
|
||||
foreach ($attributes as $key => $val) {
|
||||
$atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . $val . '"';
|
||||
}
|
||||
|
||||
return rtrim($atts, ',');
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ class Theme
|
|||
/**
|
||||
* Returns the path to the specified theme folder. If no theme is provided, will use the current theme.
|
||||
*/
|
||||
public static function path(string $theme = null): string
|
||||
public static function path(?string $theme = null): string
|
||||
{
|
||||
if ($theme === null) {
|
||||
$theme = static::current();
|
||||
|
|
|
|||
|
|
@ -12,14 +12,16 @@ namespace App\Models;
|
|||
|
||||
use App\Entities\Actor;
|
||||
use Modules\Fediverse\Models\ActorModel as FediverseActorModel;
|
||||
use Override;
|
||||
|
||||
class ActorModel extends FediverseActorModel
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Actor>
|
||||
*/
|
||||
protected $returnType = Actor::class;
|
||||
|
||||
#[Override]
|
||||
public function getActorById(int $id): ?Actor
|
||||
{
|
||||
return $this->find($id);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class CategoryModel extends Model
|
|||
protected $allowedFields = ['parent_id', 'code', 'apple_category', 'google_category'];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Category>
|
||||
*/
|
||||
protected $returnType = Category::class;
|
||||
|
||||
|
|
@ -65,12 +65,17 @@ class CategoryModel extends Model
|
|||
$options = array_reduce(
|
||||
$categories,
|
||||
static function (array $result, Category $category): array {
|
||||
$result[$category->id] = '';
|
||||
$label = '';
|
||||
if ($category->parent instanceof Category) {
|
||||
$result[$category->id] = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
|
||||
$label = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
|
||||
}
|
||||
|
||||
$result[$category->id] .= lang('Podcast.category_options.' . $category->code);
|
||||
$label .= lang('Podcast.category_options.' . $category->code);
|
||||
|
||||
$result[] = [
|
||||
'value' => $category->id,
|
||||
'label' => $label,
|
||||
];
|
||||
return $result;
|
||||
},
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ class ClipModel extends Model
|
|||
|
||||
public function __construct(
|
||||
protected string $type = 'audio',
|
||||
ConnectionInterface &$db = null,
|
||||
ValidationInterface $validation = null
|
||||
?ConnectionInterface &$db = null,
|
||||
?ValidationInterface $validation = null,
|
||||
) {
|
||||
switch ($type) {
|
||||
case 'audio':
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class CreditModel extends Model
|
|||
protected $table = 'credits';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Credit>
|
||||
*/
|
||||
protected $returnType = Credit::class;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use Modules\Fediverse\Objects\TombstoneObject;
|
|||
class EpisodeCommentModel extends UuidModel
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<EpisodeComment>
|
||||
*/
|
||||
protected $returnType = EpisodeComment::class;
|
||||
|
||||
|
|
@ -86,11 +86,13 @@ class EpisodeCommentModel extends UuidModel
|
|||
}
|
||||
|
||||
if ($comment->in_reply_to_id === null) {
|
||||
(new EpisodeModel())->builder()
|
||||
new EpisodeModel()
|
||||
->builder()
|
||||
->where('id', $comment->episode_id)
|
||||
->increment('comments_count');
|
||||
} else {
|
||||
(new self())->builder()
|
||||
new self()
|
||||
->builder()
|
||||
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
||||
->increment('replies_count');
|
||||
}
|
||||
|
|
@ -102,7 +104,7 @@ class EpisodeCommentModel extends UuidModel
|
|||
'episode-comment',
|
||||
esc($comment->actor->username),
|
||||
$comment->episode->slug,
|
||||
$comment->id
|
||||
$comment->id,
|
||||
);
|
||||
|
||||
$createActivity = new CreateActivity();
|
||||
|
|
@ -180,7 +182,8 @@ class EpisodeCommentModel extends UuidModel
|
|||
->where('id', $comment->episode_id)
|
||||
->decrement('comments_count');
|
||||
} else {
|
||||
(new self())->builder()
|
||||
new self()
|
||||
->builder()
|
||||
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
||||
->decrement('replies_count');
|
||||
}
|
||||
|
|
@ -211,7 +214,7 @@ class EpisodeCommentModel extends UuidModel
|
|||
$postModel = new PostModel();
|
||||
$episodePostsRepliesBuilder = $postModel->builder();
|
||||
$episodePostsReplies = $episodePostsRepliesBuilder->select(
|
||||
'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, is_private, 1 as is_from_post'
|
||||
'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, is_private, 1 as is_from_post',
|
||||
)
|
||||
->whereIn('in_reply_to_id', static function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
|
||||
return $builder->select('id')
|
||||
|
|
@ -232,12 +235,12 @@ class EpisodeCommentModel extends UuidModel
|
|||
|
||||
/** @var BaseResult $allEpisodeComments */
|
||||
$allEpisodeComments = $this->db->query(
|
||||
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
|
||||
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC',
|
||||
);
|
||||
|
||||
return $this->convertUuidFieldsToStrings(
|
||||
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
|
||||
$this->tempReturnType
|
||||
$this->tempReturnType,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ class EpisodeModel extends UuidModel
|
|||
'location_name',
|
||||
'location_geo',
|
||||
'location_osm',
|
||||
'custom_rss',
|
||||
'is_published_on_hubs',
|
||||
'downloads_count',
|
||||
'posts_count',
|
||||
|
|
@ -99,7 +98,7 @@ class EpisodeModel extends UuidModel
|
|||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Episode>
|
||||
*/
|
||||
protected $returnType = Episode::class;
|
||||
|
||||
|
|
@ -236,8 +235,8 @@ class EpisodeModel extends UuidModel
|
|||
public function getPodcastEpisodes(
|
||||
int $podcastId,
|
||||
string $podcastType,
|
||||
string $year = null,
|
||||
string $season = null
|
||||
?string $year = null,
|
||||
?string $season = null,
|
||||
): array {
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
|
|
@ -348,7 +347,7 @@ class EpisodeModel extends UuidModel
|
|||
{
|
||||
$result = $this->builder()
|
||||
->select(
|
||||
'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at'
|
||||
'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at',
|
||||
)
|
||||
->where('podcast_id', $podcastId)
|
||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||
|
|
@ -369,13 +368,15 @@ class EpisodeModel extends UuidModel
|
|||
|
||||
public function resetCommentsCount(): int | false
|
||||
{
|
||||
$episodeCommentsCount = (new EpisodeCommentModel())->builder()
|
||||
$episodeCommentsCount = new EpisodeCommentModel()
|
||||
->builder()
|
||||
->select('episode_id, COUNT(*) as `comments_count`')
|
||||
->where('in_reply_to_id')
|
||||
->groupBy('episode_id')
|
||||
->getCompiledSelect();
|
||||
|
||||
$episodePostsRepliesCount = (new PostModel())->builder()
|
||||
$episodePostsRepliesCount = new PostModel()
|
||||
->builder()
|
||||
->select('fediverse_posts.episode_id as episode_id, COUNT(*) as `comments_count`')
|
||||
->join('fediverse_posts as fp', 'fediverse_posts.id = fp.in_reply_to_id')
|
||||
->where('fediverse_posts.in_reply_to_id')
|
||||
|
|
@ -385,13 +386,14 @@ class EpisodeModel extends UuidModel
|
|||
|
||||
/** @var BaseResult $query */
|
||||
$query = $this->db->query(
|
||||
'SELECT `episode_id` as `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `episode_id`'
|
||||
'SELECT `episode_id` as `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `episode_id`',
|
||||
);
|
||||
|
||||
$countsPerEpisodeId = $query->getResultArray();
|
||||
|
||||
if ($countsPerEpisodeId !== []) {
|
||||
return (new self())->updateBatch($countsPerEpisodeId, 'id');
|
||||
return new self()
|
||||
->updateBatch($countsPerEpisodeId, 'id');
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -430,7 +432,8 @@ class EpisodeModel extends UuidModel
|
|||
}
|
||||
|
||||
/** @var ?Episode $episode */
|
||||
$episode = (new self())->find($episodeId);
|
||||
$episode = new self()
|
||||
->find($episodeId);
|
||||
|
||||
if (! $episode instanceof Episode) {
|
||||
return $data;
|
||||
|
|
@ -481,7 +484,7 @@ class EpisodeModel extends UuidModel
|
|||
')
|
||||
->select("{$podcastTable}.created_at AS podcast_created_at")
|
||||
->select(
|
||||
"{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown"
|
||||
"{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown",
|
||||
)
|
||||
->join($podcastTable, "{$podcastTable} on {$podcastTable}.id = {$episodeTable}.podcast_id")
|
||||
->where('
|
||||
|
|
@ -490,7 +493,7 @@ class EpisodeModel extends UuidModel
|
|||
. 'OR' .
|
||||
$podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
|
||||
. ')
|
||||
');
|
||||
', );
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
|
@ -524,7 +527,8 @@ class EpisodeModel extends UuidModel
|
|||
}
|
||||
|
||||
/** @var ?Episode $episode */
|
||||
$episode = (new self())->find($episodeId);
|
||||
$episode = new self()
|
||||
->find($episodeId);
|
||||
|
||||
if (! $episode instanceof Episode) {
|
||||
return $data;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class LanguageModel extends Model
|
|||
protected $allowedFields = ['code', 'native_name'];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Language>
|
||||
*/
|
||||
protected $returnType = Language::class;
|
||||
|
||||
|
|
@ -56,7 +56,10 @@ class LanguageModel extends Model
|
|||
$options = array_reduce(
|
||||
$languages,
|
||||
static function (array $result, Language $language): array {
|
||||
$result[$language->code] = $language->native_name;
|
||||
$result[] = [
|
||||
'value' => $language->code,
|
||||
'label' => $language->native_name,
|
||||
];
|
||||
return $result;
|
||||
},
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class LikeModel extends UuidModel
|
|||
protected $allowedFields = ['actor_id', 'comment_id'];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Like>
|
||||
*/
|
||||
protected $returnType = Like::class;
|
||||
|
||||
|
|
@ -56,7 +56,8 @@ class LikeModel extends UuidModel
|
|||
'comment_id' => $comment->id,
|
||||
]);
|
||||
|
||||
(new EpisodeCommentModel())->builder()
|
||||
new EpisodeCommentModel()
|
||||
->builder()
|
||||
->where('id', service('uuid')->fromString($comment->id)->getBytes())
|
||||
->increment('likes_count');
|
||||
|
||||
|
|
@ -91,7 +92,8 @@ class LikeModel extends UuidModel
|
|||
{
|
||||
$this->db->transStart();
|
||||
|
||||
(new EpisodeCommentModel())->builder()
|
||||
new EpisodeCommentModel()
|
||||
->builder()
|
||||
->where('id', service('uuid') ->fromString($comment->id) ->getBytes())
|
||||
->decrement('likes_count');
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class PageModel extends Model
|
|||
protected $allowedFields = ['id', 'title', 'slug', 'content_markdown', 'content_html'];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Page>
|
||||
*/
|
||||
protected $returnType = Page::class;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class PersonModel extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Person>
|
||||
*/
|
||||
protected $returnType = Person::class;
|
||||
|
||||
|
|
@ -146,7 +146,10 @@ class PersonModel extends Model
|
|||
->orderBy('`full_name`', 'ASC')
|
||||
->findAll(),
|
||||
static function (array $result, Person $person): array {
|
||||
$result[$person->id] = $person->full_name;
|
||||
$result[] = [
|
||||
'value' => $person->id,
|
||||
'label' => $person->full_name,
|
||||
];
|
||||
return $result;
|
||||
},
|
||||
[],
|
||||
|
|
@ -174,9 +177,10 @@ class PersonModel extends Model
|
|||
if (! ($options = cache($cacheName))) {
|
||||
foreach ($personsTaxonomy as $group_key => $group) {
|
||||
foreach ($group['roles'] as $role_key => $role) {
|
||||
$options[
|
||||
"{$group_key},{$role_key}"
|
||||
] = "{$group['label']} › {$role['label']}";
|
||||
$options[] = [
|
||||
'value' => sprintf('%s,%s', $group_key, $role_key),
|
||||
'label' => sprintf('%s › %s', $group['label'], $role['label']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +215,7 @@ class PersonModel extends Model
|
|||
if (! ($found = cache($cacheName))) {
|
||||
$this->builder()
|
||||
->select(
|
||||
'persons.*, episodes_persons.podcast_id as podcast_id, episodes_persons.episode_id as episode_id'
|
||||
'persons.*, episodes_persons.podcast_id as podcast_id, episodes_persons.episode_id as episode_id',
|
||||
)
|
||||
->distinct()
|
||||
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
|
||||
|
|
@ -253,7 +257,7 @@ class PersonModel extends Model
|
|||
int $episodeId,
|
||||
int $personId,
|
||||
string $groupSlug,
|
||||
string $roleSlug
|
||||
string $roleSlug,
|
||||
): bool {
|
||||
return $this->db->table('episodes_persons')
|
||||
->insert([
|
||||
|
|
@ -293,9 +297,10 @@ class PersonModel extends Model
|
|||
|
||||
cache()
|
||||
->delete("podcast#{$podcastId}_persons");
|
||||
(new PodcastModel())->clearCache([
|
||||
'id' => $podcastId,
|
||||
]);
|
||||
new PodcastModel()
|
||||
->clearCache([
|
||||
'id' => $podcastId,
|
||||
]);
|
||||
|
||||
$data = [];
|
||||
foreach ($personIds as $personId) {
|
||||
|
|
@ -335,9 +340,10 @@ class PersonModel extends Model
|
|||
cache()->deleteMatching("podcast#{$podcastId}_person#{$personId}*");
|
||||
cache()
|
||||
->delete("podcast#{$podcastId}_persons");
|
||||
(new PodcastModel())->clearCache([
|
||||
'id' => $podcastId,
|
||||
]);
|
||||
new PodcastModel()
|
||||
->clearCache([
|
||||
'id' => $podcastId,
|
||||
]);
|
||||
|
||||
return $this->db->table('podcasts_persons')
|
||||
->delete([
|
||||
|
|
@ -359,9 +365,10 @@ class PersonModel extends Model
|
|||
if ($personIds !== []) {
|
||||
cache()
|
||||
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
||||
(new EpisodeModel())->clearCache([
|
||||
'id' => $episodeId,
|
||||
]);
|
||||
new EpisodeModel()
|
||||
->clearCache([
|
||||
'id' => $episodeId,
|
||||
]);
|
||||
|
||||
$data = [];
|
||||
foreach ($personIds as $personId) {
|
||||
|
|
@ -400,9 +407,10 @@ class PersonModel extends Model
|
|||
cache()->deleteMatching("podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}*");
|
||||
cache()
|
||||
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
||||
(new EpisodeModel())->clearCache([
|
||||
'id' => $episodeId,
|
||||
]);
|
||||
new EpisodeModel()
|
||||
->clearCache([
|
||||
'id' => $episodeId,
|
||||
]);
|
||||
|
||||
return $this->db->table('episodes_persons')
|
||||
->delete([
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ class PodcastModel extends Model
|
|||
'handle',
|
||||
'description_markdown',
|
||||
'description_html',
|
||||
'episode_description_footer_markdown',
|
||||
'episode_description_footer_html',
|
||||
'cover_id',
|
||||
'banner_id',
|
||||
'language_code',
|
||||
|
|
@ -47,10 +45,8 @@ class PodcastModel extends Model
|
|||
'parental_advisory',
|
||||
'owner_name',
|
||||
'owner_email',
|
||||
'is_owner_email_removed_from_feed',
|
||||
'publisher',
|
||||
'type',
|
||||
'medium',
|
||||
'copyright',
|
||||
'imported_feed_url',
|
||||
'new_feed_url',
|
||||
|
|
@ -60,13 +56,7 @@ class PodcastModel extends Model
|
|||
'location_name',
|
||||
'location_geo',
|
||||
'location_osm',
|
||||
'verify_txt',
|
||||
'payment_pointer',
|
||||
'custom_rss',
|
||||
'is_published_on_hubs',
|
||||
'partner_id',
|
||||
'partner_link_url',
|
||||
'partner_image_url',
|
||||
'is_premium_by_default',
|
||||
'published_at',
|
||||
'created_by',
|
||||
|
|
@ -74,7 +64,7 @@ class PodcastModel extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Podcast>
|
||||
*/
|
||||
protected $returnType = Podcast::class;
|
||||
|
||||
|
|
@ -173,7 +163,7 @@ class PodcastModel extends Model
|
|||
/**
|
||||
* @return Podcast[]
|
||||
*/
|
||||
public function getAllPodcasts(string $orderBy = null): array
|
||||
public function getAllPodcasts(?string $orderBy = null): array
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
||||
|
|
@ -185,7 +175,7 @@ class PodcastModel extends Model
|
|||
->where(
|
||||
'`' . $prefix . 'fediverse_posts`.`published_at` <= UTC_TIMESTAMP()',
|
||||
null,
|
||||
false
|
||||
false,
|
||||
)->orWhere('fediverse_posts.published_at')
|
||||
->groupEnd()
|
||||
->groupBy('podcasts.actor_id')
|
||||
|
|
@ -319,7 +309,8 @@ class PodcastModel extends Model
|
|||
];
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId);
|
||||
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||
->getSecondsToNextUnpublishedEpisode($podcastId);
|
||||
|
||||
cache()
|
||||
->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
|
||||
|
|
@ -335,7 +326,8 @@ class PodcastModel extends Model
|
|||
*/
|
||||
public function clearCache(array $data): array
|
||||
{
|
||||
$podcast = (new self())->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
$podcast = new self()
|
||||
->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
|
||||
// delete cache for users' podcasts
|
||||
cache()
|
||||
|
|
@ -399,21 +391,22 @@ class PodcastModel extends Model
|
|||
$domain =
|
||||
$url->getHost() . ($url->getPort() ? ':' . $url->getPort() : '');
|
||||
|
||||
$actorId = (new ActorModel())->insert(
|
||||
[
|
||||
'uri' => url_to('podcast-activity', $username),
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'private_key' => $privatekey,
|
||||
'public_key' => $publickey,
|
||||
'display_name' => $data['data']['title'],
|
||||
'summary' => $data['data']['description_html'],
|
||||
'inbox_url' => url_to('inbox', $username),
|
||||
'outbox_url' => url_to('outbox', $username),
|
||||
'followers_url' => url_to('followers', $username),
|
||||
],
|
||||
true,
|
||||
);
|
||||
$actorId = new ActorModel()
|
||||
->insert(
|
||||
[
|
||||
'uri' => url_to('podcast-activity', $username),
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'private_key' => $privatekey,
|
||||
'public_key' => $publickey,
|
||||
'display_name' => $data['data']['title'],
|
||||
'summary' => $data['data']['description_html'],
|
||||
'inbox_url' => url_to('inbox', $username),
|
||||
'outbox_url' => url_to('outbox', $username),
|
||||
'followers_url' => url_to('followers', $username),
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
$data['data']['actor_id'] = $actorId;
|
||||
|
||||
|
|
@ -427,10 +420,12 @@ class PodcastModel extends Model
|
|||
*/
|
||||
protected function setActorAvatar(array $data): array
|
||||
{
|
||||
$podcast = (new self())->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
$podcast = new self()
|
||||
->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
|
||||
if ($podcast instanceof Podcast) {
|
||||
$podcastActor = (new ActorModel())->find($podcast->actor_id);
|
||||
$podcastActor = new ActorModel()
|
||||
->find($podcast->actor_id);
|
||||
|
||||
if (! $podcastActor instanceof Actor) {
|
||||
return $data;
|
||||
|
|
@ -439,7 +434,8 @@ class PodcastModel extends Model
|
|||
$podcastActor->avatar_image_url = $podcast->cover->federation_url;
|
||||
$podcastActor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
|
||||
|
||||
(new ActorModel())->update($podcast->actor_id, $podcastActor);
|
||||
new ActorModel()
|
||||
->update($podcast->actor_id, $podcastActor);
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
|
@ -452,7 +448,8 @@ class PodcastModel extends Model
|
|||
*/
|
||||
protected function updatePodcastActor(array $data): array
|
||||
{
|
||||
$podcast = (new self())->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
$podcast = new self()
|
||||
->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
|
||||
|
||||
if ($podcast instanceof Podcast) {
|
||||
$actorModel = new ActorModel();
|
||||
|
|
@ -488,7 +485,7 @@ class PodcastModel extends Model
|
|||
{
|
||||
if (! array_key_exists(
|
||||
'guid',
|
||||
$data['data']
|
||||
$data['data'],
|
||||
) || $data['data']['guid'] === null || $data['data']['guid'] === '') {
|
||||
$uuid = service('uuid');
|
||||
$feedUrl = url_to('podcast-rss-feed', $data['data']['handle']);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use Modules\Fediverse\Models\PostModel as FediversePostModel;
|
|||
class PostModel extends FediversePostModel
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var class-string<Post>
|
||||
*/
|
||||
protected $returnType = Post::class;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ declare(strict_types=1);
|
|||
namespace App\Validation;
|
||||
|
||||
use CodeIgniter\Validation\FileRules as ValidationFileRules;
|
||||
use Override;
|
||||
|
||||
class FileRules extends ValidationFileRules
|
||||
{
|
||||
/**
|
||||
* Checks an uploaded file to verify that the dimensions are within a specified allowable dimension.
|
||||
*/
|
||||
public function min_dims(string $blank = null, string $params = ''): bool
|
||||
#[Override]
|
||||
public function min_dims(?string $blank = null, string $params = ''): bool
|
||||
{
|
||||
// Grab the file name off the top of the $params
|
||||
// after we split it.
|
||||
|
|
@ -59,7 +61,7 @@ class FileRules extends ValidationFileRules
|
|||
/**
|
||||
* Checks an uploaded image to verify that the ratio corresponds to the params
|
||||
*/
|
||||
public function is_image_ratio(string $blank = null, string $params = ''): bool
|
||||
public function is_image_ratio(?string $blank = null, string $params = ''): bool
|
||||
{
|
||||
// Grab the file name off the top of the $params
|
||||
// after we split it.
|
||||
|
|
@ -99,7 +101,7 @@ class FileRules extends ValidationFileRules
|
|||
/**
|
||||
* Checks that an uploaded json file's content is valid
|
||||
*/
|
||||
public function is_json(string $blank = null, string $params = ''): bool
|
||||
public function is_json(?string $blank = null, string $params = ''): bool
|
||||
{
|
||||
// Grab the file name off the top of the $params
|
||||
// after we split it.
|
||||
|
|
|
|||
29
app/Validation/OtherRules.php
Normal file
29
app/Validation/OtherRules.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validation;
|
||||
|
||||
class OtherRules
|
||||
{
|
||||
/**
|
||||
* Is a boolean (true or false)
|
||||
*/
|
||||
public function is_boolean(mixed $str = null): bool
|
||||
{
|
||||
return is_bool($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it an array?
|
||||
*/
|
||||
public function is_list(mixed $str = null): bool
|
||||
{
|
||||
return is_array($str);
|
||||
}
|
||||
|
||||
public function is_string_or_list(mixed $str = null): bool
|
||||
{
|
||||
return is_string($str) || is_array($str);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,22 +4,30 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Alert extends Component
|
||||
{
|
||||
protected ?string $glyph = null;
|
||||
protected array $props = ['glyph', 'title', 'variant'];
|
||||
|
||||
protected ?string $title = null;
|
||||
protected string $glyph = '';
|
||||
|
||||
protected ?string $title = '';
|
||||
|
||||
protected array $attributes = [
|
||||
'role' => 'alert',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var 'default'|'success'|'danger'|'warning'
|
||||
*/
|
||||
protected string $variant = 'default';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$variants = [
|
||||
$variantData = match ($this->variant) {
|
||||
'success' => [
|
||||
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'glyph' => 'check-fill', // @icon("check-fill")
|
||||
|
|
@ -32,30 +40,21 @@ class Alert extends Component
|
|||
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
'glyph' => 'alert-fill', // @icon("alert-fill")
|
||||
],
|
||||
'default' => [
|
||||
default => [
|
||||
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
|
||||
'glyph' => 'error-warning-fill', // @icon("error-warning-fill")
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
if (! array_key_exists($this->variant, $variants)) {
|
||||
$this->variant = 'default';
|
||||
}
|
||||
|
||||
$glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [
|
||||
$glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [
|
||||
'class' => 'flex-shrink-0 mr-2 text-lg',
|
||||
]);
|
||||
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
||||
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
unset($this->attributes['variant']);
|
||||
unset($this->attributes['class']);
|
||||
unset($this->attributes['glyph']);
|
||||
$attributes = stringify_attributes($this->attributes);
|
||||
$title = $this->title === '' ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
||||
$this->mergeClass('inline-flex w-full p-2 text-sm border rounded ');
|
||||
$this->mergeClass($variantData['class']);
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$class}" role="alert" {$attributes}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
|
||||
<div {$this->getStringifiedAttributes()}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,25 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Button extends Component
|
||||
{
|
||||
protected array $props = ['uri', 'variant', 'size', 'iconLeft', 'iconRight', 'isSquared', 'isExternal'];
|
||||
|
||||
protected array $casts = [
|
||||
'isSquared' => 'boolean',
|
||||
'isExternal' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $uri = '';
|
||||
|
||||
protected string $variant = 'default';
|
||||
|
||||
/**
|
||||
* @var 'small'|'base'|'large'
|
||||
*/
|
||||
protected string $size = 'base';
|
||||
|
||||
protected string $iconLeft = '';
|
||||
|
|
@ -20,65 +31,54 @@ class Button extends Component
|
|||
|
||||
protected bool $isSquared = false;
|
||||
|
||||
public function setIsSquared(string $value): void
|
||||
{
|
||||
$this->isSquared = $value === 'true';
|
||||
}
|
||||
protected bool $isExternal = false;
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$baseClass =
|
||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
|
||||
$this->mergeClass('shadow gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full');
|
||||
|
||||
$variantClass = [
|
||||
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
||||
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||
'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
||||
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
||||
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
||||
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
||||
'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
|
||||
'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
|
||||
];
|
||||
$variantClass = match ($this->variant) {
|
||||
'primary' => 'text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||
'secondary' => 'ring-2 ring-accent-base ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover hover:ring-accent-hover',
|
||||
'danger' => 'bg-red-50 ring-2 ring-red-700 ring-inset text-red-700 hover:ring-red-800 hover:text-red-800',
|
||||
'warning' => 'bg-yellow-50 ring-2 ring-yellow-700 ring-inset text-yellow-700 hover:ring-yellow-800 hover:text-yellow-800',
|
||||
'info' => 'bg-blue-50 ring-2 ring-blue-700 ring-inset text-blue-700 hover:ring-blue-800 hover:text-blue-800',
|
||||
'disabled' => 'text-black bg-gray-300 cursor-not-allowed',
|
||||
default => 'text-black bg-gray-50 hover:bg-gray-200',
|
||||
};
|
||||
|
||||
$sizeClass = [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'text-xs leading-6',
|
||||
'base' => 'text-sm leading-5',
|
||||
'large' => 'text-base leading-6',
|
||||
];
|
||||
default => 'text-sm leading-5',
|
||||
};
|
||||
|
||||
$iconSize = [
|
||||
$iconSizeClass = match ($this->size) {
|
||||
'small' => 'text-sm',
|
||||
'base' => 'text-lg',
|
||||
'large' => 'text-2xl',
|
||||
];
|
||||
default => 'text-lg',
|
||||
};
|
||||
|
||||
$basePaddings = [
|
||||
$basePaddings = match ($this->size) {
|
||||
'small' => 'px-3 py-1',
|
||||
'base' => 'px-3 py-2',
|
||||
'large' => 'px-4 py-2',
|
||||
];
|
||||
default => 'px-3 py-2',
|
||||
};
|
||||
|
||||
$squaredPaddings = [
|
||||
$squaredPaddings = match ($this->size) {
|
||||
'small' => 'p-1',
|
||||
'base' => 'p-2',
|
||||
'large' => 'p-3',
|
||||
];
|
||||
default => 'p-2',
|
||||
};
|
||||
|
||||
$buttonClass =
|
||||
$baseClass .
|
||||
' ' .
|
||||
($this->isSquared
|
||||
? $squaredPaddings[$this->size]
|
||||
: $basePaddings[$this->size]) .
|
||||
' ' .
|
||||
$sizeClass[$this->size] .
|
||||
' ' .
|
||||
$variantClass[$this->variant];
|
||||
$this->mergeClass($variantClass);
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
if (array_key_exists('class', $this->attributes)) {
|
||||
$buttonClass .= ' ' . $this->attributes['class'];
|
||||
unset($this->attributes['class']);
|
||||
if ($this->isSquared) {
|
||||
$this->mergeClass($squaredPaddings);
|
||||
} else {
|
||||
$this->mergeClass($basePaddings);
|
||||
}
|
||||
|
||||
if ($this->iconLeft !== '' || $this->iconRight !== '') {
|
||||
|
|
@ -87,41 +87,30 @@ class Button extends Component
|
|||
|
||||
if ($this->iconLeft !== '') {
|
||||
$this->slot = icon($this->iconLeft, [
|
||||
'class' => 'opacity-75 ' . $iconSize[$this->size],
|
||||
'class' => 'opacity-75 ' . $iconSizeClass,
|
||||
]) . $this->slot;
|
||||
}
|
||||
|
||||
if ($this->iconRight !== '') {
|
||||
$this->slot .= icon($this->iconRight, [
|
||||
'class' => 'opacity-75 ' . $iconSize[$this->size],
|
||||
'class' => 'opacity-75 ' . $iconSizeClass,
|
||||
]);
|
||||
}
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
unset($this->attributes['variant']);
|
||||
unset($this->attributes['size']);
|
||||
unset($this->attributes['iconLeft']);
|
||||
unset($this->attributes['iconRight']);
|
||||
unset($this->attributes['isSquared']);
|
||||
unset($this->attributes['uri']);
|
||||
unset($this->attributes['label']);
|
||||
|
||||
if ($this->uri !== '') {
|
||||
$tagName = 'a';
|
||||
$defaultButtonAttributes = [
|
||||
'href' => $this->uri,
|
||||
];
|
||||
$this->attributes['href'] = $this->uri;
|
||||
if ($this->isExternal) {
|
||||
$this->attributes['target'] = '_blank';
|
||||
$this->attributes['rel'] = 'noopener noreferrer';
|
||||
}
|
||||
} else {
|
||||
$tagName = 'button';
|
||||
$defaultButtonAttributes = [
|
||||
'type' => 'button',
|
||||
];
|
||||
$this->attributes['type'] ??= 'button';
|
||||
}
|
||||
|
||||
$attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
|
||||
|
||||
return <<<HTML
|
||||
<{$tagName} class="{$buttonClass}" {$attributes}>{$this->slot}</{$tagName}>
|
||||
<{$tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$tagName}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Charts;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class ChartsComponent extends Component
|
||||
{
|
||||
protected string $title = '';
|
||||
protected array $props = ['title', 'subtitle', 'dataUrl', 'type'];
|
||||
|
||||
protected string $title;
|
||||
|
||||
protected string $subtitle = '';
|
||||
|
||||
protected string $dataUrl = '';
|
||||
protected string $dataUrl;
|
||||
|
||||
protected string $type = '';
|
||||
protected string $type;
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$subtitleBlock = '';
|
||||
|
|
@ -23,8 +27,10 @@ class ChartsComponent extends Component
|
|||
$subtitleBlock = '<p class="px-6 -mt-4 text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
}
|
||||
|
||||
$this->mergeClass('bg-elevated border-3 rounded-xl border-subtle');
|
||||
|
||||
return <<<HTML
|
||||
<div class="bg-elevated border-3 rounded-xl border-subtle {$this->class}">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<h2 class="px-6 py-4 text-xl">{$this->title}</h2>
|
||||
{$subtitleBlock}
|
||||
<div class="w-full h-[500px]" data-chart-type="{$this->type}" data-chart-url="{$this->dataUrl}"></div>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class DashboardCard extends Component
|
||||
{
|
||||
protected ?string $href = null;
|
||||
protected array $props = ['href', 'glyph', 'title', 'subtitle'];
|
||||
|
||||
protected string $href = '';
|
||||
|
||||
protected string $glyph;
|
||||
|
||||
|
|
@ -21,17 +24,18 @@ class DashboardCard extends Component
|
|||
$this->subtitle = html_entity_decode($value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$glyph = (string) icon($this->glyph, [
|
||||
'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base',
|
||||
]);
|
||||
|
||||
if ($this->href !== null && $this->href !== '') {
|
||||
$chevronRight = (string) icon('arrow-right-s-fill');
|
||||
if ($this->href !== '') {
|
||||
$chevronRight = icon('arrow-right-s-fill');
|
||||
$viewLang = lang('Common.view');
|
||||
return <<<HTML
|
||||
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated focus:ring-accent rounded-xl border-3 border-subtle group">
|
||||
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated rounded-xl border-3 border-subtle group">
|
||||
<div class="flex items-start">{$glyph}<div class="flex flex-col ml-2"><div class="flex items-center"><span class="text-xs font-semibold leading-loose tracking-wider uppercase">{$this->title}</span><div class="inline-flex items-center ml-4 transition -translate-x-full group-hover:translate-x-0 group-focus:translate-x-0"><span class="-ml-2 text-xs lowercase transition opacity-0 group-hover:opacity-100 group-focus:opacity-100">{$viewLang}</span>{$chevronRight}</div></div><p class="text-xs">{$this->subtitle}</p></div></div>
|
||||
<div class="text-5xl font-bold">{$this->slot}</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -5,27 +5,37 @@ declare(strict_types=1);
|
|||
namespace App\Views\Components;
|
||||
|
||||
use Exception;
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class DropdownMenu extends Component
|
||||
{
|
||||
public string $id = '';
|
||||
protected array $props = ['id', 'labelledby', 'placement', 'offsetX', 'offsetY', 'items'];
|
||||
|
||||
public string $labelledby;
|
||||
protected array $casts = [
|
||||
'offsetX' => 'number',
|
||||
'offsetY' => 'number',
|
||||
'items' => 'array',
|
||||
];
|
||||
|
||||
public string $placement = 'bottom-end';
|
||||
protected string $id;
|
||||
|
||||
public string $offsetX = '0';
|
||||
protected string $labelledby;
|
||||
|
||||
public string $offsetY = '0';
|
||||
protected string $placement = 'bottom-end';
|
||||
|
||||
public array $items = [];
|
||||
protected int $offsetX = 0;
|
||||
|
||||
protected int $offsetY = 0;
|
||||
|
||||
protected array $items = [];
|
||||
|
||||
public function setItems(string $value): void
|
||||
{
|
||||
$this->items = json_decode(htmlspecialchars_decode($value), true);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
if ($this->items === []) {
|
||||
|
|
@ -37,7 +47,7 @@ class DropdownMenu extends Component
|
|||
switch ($item['type']) {
|
||||
case 'link':
|
||||
$menuItems .= anchor($item['uri'], $item['title'], [
|
||||
'class' => 'px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
]);
|
||||
break;
|
||||
case 'html':
|
||||
|
|
@ -51,14 +61,16 @@ class DropdownMenu extends Component
|
|||
}
|
||||
}
|
||||
|
||||
$this->mergeClass('absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3');
|
||||
$this->attributes['id'] = $this->id;
|
||||
$this->attributes['aria-labelledby'] = $this->labelledby;
|
||||
$this->attributes['data-dropdown'] = 'menu';
|
||||
$this->attributes['data-dropdown-placement'] = $this->placement;
|
||||
$this->attributes['data-dropdown-offset-x'] = $this->offsetX;
|
||||
$this->attributes['data-dropdown-offset-y'] = $this->offsetY;
|
||||
|
||||
return <<<HTML
|
||||
<nav id="{$this->id}"
|
||||
class="absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3"
|
||||
aria-labelledby="{$this->labelledby}"
|
||||
data-dropdown="menu"
|
||||
data-dropdown-placement="{$this->placement}"
|
||||
data-dropdown-offset-x="{$this->offsetX}"
|
||||
data-dropdown-offset-y="{$this->offsetY}">{$menuItems}</nav>
|
||||
<nav {$this->getStringifiedAttributes()}>{$menuItems}</nav>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,35 +4,59 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
use Override;
|
||||
|
||||
class Checkbox extends FormComponent
|
||||
{
|
||||
protected ?string $hint = null;
|
||||
protected array $props = ['hint', 'helper'];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
protected string $hint = '';
|
||||
|
||||
protected string $helper = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$attributes = [
|
||||
'id' => $this->value,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 focus:ring-accent w-6 h-6',
|
||||
];
|
||||
|
||||
$checkboxInput = form_checkbox(
|
||||
$attributes,
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 focus:ring-accent w-6 h-6 transition',
|
||||
],
|
||||
'yes',
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
in_array($this->getValue(), ['yes', 'true', 'on', '1'], true),
|
||||
);
|
||||
|
||||
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
$hint = $this->hint === '' ? '' : new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
])->render();
|
||||
|
||||
$this->mergeClass('inline-flex items-start gap-x-2');
|
||||
|
||||
$helperText = '';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
'class' => '-mt-1',
|
||||
])->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<label class="inline-flex items-center {$this->class}">{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
|
||||
<label {$this->getStringifiedAttributes()}>{$checkboxInput}
|
||||
<div class="flex flex-col">
|
||||
<span>{$this->slot}{$hint}</span>
|
||||
{$helperText}
|
||||
</div>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
app/Views/Components/Forms/CodeEditor.php
Normal file
35
app/Views/Components/Forms/CodeEditor.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class CodeEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['content', 'lang'];
|
||||
|
||||
protected array $attributes = [
|
||||
'rows' => '6',
|
||||
'class' => 'bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent transition',
|
||||
];
|
||||
|
||||
protected string $lang = '';
|
||||
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
$this->value = htmlspecialchars_decode($value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['slot'] = 'textarea';
|
||||
$textarea = form_textarea($this->attributes, $this->getValue());
|
||||
|
||||
return <<<HTML
|
||||
<code-editor lang="{$this->lang}">{$textarea}</code-editor>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,19 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class ColorRadioButton extends FormComponent
|
||||
{
|
||||
protected bool $isChecked = false;
|
||||
protected array $props = ['isSelected'];
|
||||
|
||||
protected string $style = '';
|
||||
protected array $casts = [
|
||||
'isSelected' => 'boolean',
|
||||
];
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
protected bool $isSelected = false;
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$data = [
|
||||
|
|
@ -23,18 +25,18 @@ class ColorRadioButton extends FormComponent
|
|||
'class' => 'color-radio-btn',
|
||||
];
|
||||
|
||||
if ($this->required) {
|
||||
if ($this->isRequired) {
|
||||
$data['required'] = 'required';
|
||||
}
|
||||
|
||||
$radioInput = form_radio(
|
||||
$data,
|
||||
$this->value,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||
);
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$this->class}" style="{$this->style}">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
{$radioInput}
|
||||
<label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,34 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class DatetimePicker extends FormComponent
|
||||
{
|
||||
protected array $attributes = [
|
||||
'data-picker' => 'datetime',
|
||||
];
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['class'] = 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0';
|
||||
$this->attributes['data-input'] = '';
|
||||
$dateInput = form_input($this->attributes, old($this->name, $this->value));
|
||||
$dateInput = form_input([
|
||||
'name' => $this->name,
|
||||
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
|
||||
'data-input' => '',
|
||||
], $this->getValue());
|
||||
|
||||
$clearLabel = lang(
|
||||
'Episode.publish_form.scheduled_publication_date_clear',
|
||||
);
|
||||
$closeIcon = icon('close-fill');
|
||||
|
||||
$this->mergeClass('flex border-3 rounded-lg border-contrast focus-within:ring-accent transition');
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex border-3 rounded-lg border-contrast focus-within:ring-accent {$this->class}" data-picker="datetime">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
{$dateInput}
|
||||
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset focus:ring-accent" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
|
||||
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
|
||||
{$closeIcon}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,51 +4,80 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class Field extends FormComponent
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Field extends Component
|
||||
{
|
||||
protected array $props = [
|
||||
'name',
|
||||
'label',
|
||||
'isRequired',
|
||||
'isReadonly',
|
||||
'as',
|
||||
'hint',
|
||||
'helper',
|
||||
];
|
||||
|
||||
protected array $casts = [
|
||||
'isRequired' => 'boolean',
|
||||
'isReadonly' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $name;
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected bool $isRequired = false;
|
||||
|
||||
protected bool $isReadonly = false;
|
||||
|
||||
protected string $as = 'Input';
|
||||
|
||||
protected string $label = '';
|
||||
protected string $hint = '';
|
||||
|
||||
protected ?string $helper = null;
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected string $helper = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$helperText = '';
|
||||
if ($this->helper !== null) {
|
||||
$helperId = $this->id . 'Help';
|
||||
$helperText = '<Forms.Helper id="' . $helperId . '">' . $this->helper . '</Forms.Helper>';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
])->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
$labelAttributes = [
|
||||
'for' => $this->id,
|
||||
'isOptional' => $this->required ? 'false' : 'true',
|
||||
'for' => $this->name,
|
||||
'isOptional' => $this->isRequired ? 'false' : 'true',
|
||||
'class' => '-mb-1',
|
||||
'slot' => $this->label,
|
||||
];
|
||||
if ($this->hint) {
|
||||
if ($this->hint !== '') {
|
||||
$labelAttributes['hint'] = $this->hint;
|
||||
}
|
||||
$labelAttributes = stringify_attributes($labelAttributes);
|
||||
$label = new Label($labelAttributes);
|
||||
|
||||
// remove field specific attributes to inject the rest to Form Component
|
||||
$fieldComponentAttributes = $this->attributes;
|
||||
unset($fieldComponentAttributes['as']);
|
||||
unset($fieldComponentAttributes['label']);
|
||||
unset($fieldComponentAttributes['class']);
|
||||
unset($fieldComponentAttributes['helper']);
|
||||
unset($fieldComponentAttributes['hint']);
|
||||
$this->mergeClass('flex flex-col');
|
||||
$fieldClass = $this->attributes['class'];
|
||||
|
||||
unset($this->attributes['class']);
|
||||
|
||||
$this->attributes['name'] = $this->name;
|
||||
$this->attributes['isRequired'] = var_export($this->isRequired, true);
|
||||
$this->attributes['isReadonly'] = var_export($this->isReadonly, true);
|
||||
$element = __NAMESPACE__ . '\\' . $this->as;
|
||||
$fieldElement = new $element($fieldComponentAttributes);
|
||||
$fieldElement = new $element($this->attributes);
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex flex-col {$this->class}">
|
||||
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
|
||||
<div class="{$fieldClass}">
|
||||
{$label->render()}
|
||||
{$helperText}
|
||||
<div class="w-full mt-1">
|
||||
<div class="relative w-full mt-1">
|
||||
{$fieldElement->render()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,52 +6,77 @@ namespace App\Views\Components\Forms;
|
|||
|
||||
use ViewComponents\Component;
|
||||
|
||||
class FormComponent extends Component
|
||||
abstract class FormComponent extends Component
|
||||
{
|
||||
protected ?string $id = null;
|
||||
protected array $props = [
|
||||
'id',
|
||||
'name',
|
||||
'value',
|
||||
'defaultValue',
|
||||
'isRequired',
|
||||
'isReadonly',
|
||||
];
|
||||
|
||||
protected string $name = '';
|
||||
protected array $casts = [
|
||||
'isRequired' => 'boolean',
|
||||
'isReadonly' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $value = '';
|
||||
protected string $id;
|
||||
|
||||
protected bool $required = false;
|
||||
protected string $name;
|
||||
|
||||
protected bool $readonly = false;
|
||||
/**
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
protected string|array|null $value = null;
|
||||
|
||||
/**
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
protected string|array|null $defaultValue = null;
|
||||
|
||||
protected bool $isRequired = false;
|
||||
|
||||
protected bool $isReadonly = false;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
$parentVars = get_class_vars(self::class);
|
||||
$this->casts = [...$parentVars['casts'], ...$this->casts];
|
||||
$this->props = [...$parentVars['props'], $this->props];
|
||||
|
||||
parent::__construct($attributes);
|
||||
|
||||
if ($this->id === null) {
|
||||
if (! isset($this->id)) {
|
||||
$this->id = $this->name;
|
||||
$this->attributes['id'] = $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
$this->value = htmlspecialchars_decode($value, ENT_QUOTES);
|
||||
}
|
||||
$this->attributes['id'] = $this->id;
|
||||
$this->attributes['name'] = $this->name;
|
||||
|
||||
public function setRequired(string $value): void
|
||||
{
|
||||
$this->required = $value === 'true';
|
||||
unset($this->attributes['required']);
|
||||
if ($this->required) {
|
||||
if ($this->isRequired) {
|
||||
$this->attributes['required'] = 'required';
|
||||
}
|
||||
}
|
||||
|
||||
public function setReadonly(string $value): void
|
||||
{
|
||||
$this->readonly = $value === 'true';
|
||||
if ($this->readonly) {
|
||||
if ($this->isReadonly) {
|
||||
$this->attributes['readonly'] = 'readonly';
|
||||
} else {
|
||||
unset($this->attributes['readonly']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getValue(): string|array
|
||||
{
|
||||
$valueCast = $this->casts['value'] ?? '';
|
||||
if ($valueCast === 'array') {
|
||||
return old($this->name, in_array($this->value, [[], null], true) ? $this->defaultValue : $this->value) ?? [];
|
||||
}
|
||||
|
||||
return old(
|
||||
$this->name,
|
||||
in_array($this->value, ['', null], true) ? $this->defaultValue : $this->value,
|
||||
) ?? '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,20 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class Helper extends FormComponent
|
||||
{
|
||||
/**
|
||||
* @var 'default'|'error'
|
||||
*/
|
||||
protected string $type = 'default';
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Helper extends Component
|
||||
{
|
||||
// TODO: add type with error and show errors inline
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$class = 'text-skin-muted';
|
||||
$this->mergeClass('form-helper');
|
||||
|
||||
return <<<HTML
|
||||
<small id="{$this->id}" class="{$class} {$this->class}">{$this->slot}</small>
|
||||
<small {$this->getStringifiedAttributes()}>{$this->slot}</small>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,33 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class Input extends FormComponent
|
||||
{
|
||||
protected array $props = ['type'];
|
||||
|
||||
protected string $type = 'text';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$baseClass = 'w-full border-contrast rounded-lg focus:border-contrast border-3 focus:ring-accent focus-within:ring-accent ' . $this->class;
|
||||
|
||||
$this->attributes['class'] = $baseClass;
|
||||
$this->mergeClass('w-full border-contrast rounded-lg focus:border-contrast border-3 focus-within:ring-accent transition');
|
||||
|
||||
if ($this->type === 'file') {
|
||||
$this->attributes['class'] .= ' file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer';
|
||||
$this->mergeClass('file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-accent-hover file:text-sm file:rounded-none file:border-none file:bg-base file:cursor-pointer');
|
||||
} else {
|
||||
$this->attributes['class'] .= ' px-3 py-2';
|
||||
$this->mergeClass('px-3 py-2');
|
||||
}
|
||||
|
||||
if ($this->readonly) {
|
||||
$this->attributes['class'] .= ' bg-base';
|
||||
if ($this->isReadonly) {
|
||||
$this->mergeClass('bg-base');
|
||||
} else {
|
||||
$this->attributes['class'] .= ' bg-elevated';
|
||||
$this->mergeClass('bg-elevated');
|
||||
}
|
||||
|
||||
return form_input($this->attributes, old($this->name, $this->value));
|
||||
$this->attributes['type'] = $this->type;
|
||||
|
||||
return form_input($this->attributes, $this->getValue());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,39 +4,42 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Label extends Component
|
||||
{
|
||||
protected ?string $for = null;
|
||||
protected array $props = ['for', 'hint', 'isOptional'];
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected array $casts = [
|
||||
'isOptional' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $for;
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $isOptional = false;
|
||||
|
||||
public function setIsOptional(string $value): void
|
||||
{
|
||||
$this->isOptional = $value === 'true';
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$labelClass = 'text-sm font-semibold ' . $this->attributes['class'];
|
||||
unset($this->attributes['class']);
|
||||
$this->mergeClass('text-sm font-semibold');
|
||||
|
||||
$optionalText = $this->isOptional ? '<small class="ml-1 font-normal lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' : '';
|
||||
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
|
||||
unset($this->attributes['isOptional']);
|
||||
unset($this->attributes['hint']);
|
||||
unset($this->attributes['slot']);
|
||||
$hint = $this->hint === '' ? '' : new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
])->render();
|
||||
|
||||
$attributes = stringify_attributes($this->attributes);
|
||||
$this->attributes['for'] = $this->for;
|
||||
|
||||
return <<<HTML
|
||||
<label class="{$labelClass}" {$attributes}>{$this->slot}{$optionalText}{$hint}</label>
|
||||
<label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class MarkdownEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['disallowList'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
|
|
@ -16,20 +20,23 @@ class MarkdownEditor extends FormComponent
|
|||
$this->disallowList = explode(',', $value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$editorClass = 'w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent ' . $this->class;
|
||||
$this->mergeClass('w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent transition');
|
||||
$wrapperClass = $this->attributes['class'];
|
||||
|
||||
$this->attributes['class'] = 'bg-elevated border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
|
||||
$this->attributes['rows'] = 6;
|
||||
|
||||
$textarea = form_textarea($this->attributes, old($this->name, $this->value));
|
||||
$markdownIcon = (string) icon(
|
||||
'markdown-fill',
|
||||
[
|
||||
'class' => 'mr-1 text-lg opacity-40',
|
||||
]
|
||||
$textarea = form_textarea(
|
||||
$this->attributes,
|
||||
$this->getValue(),
|
||||
);
|
||||
$markdownIcon = (string) icon('markdown-fill', [
|
||||
'class' => 'mr-1 text-lg opacity-40',
|
||||
]);
|
||||
|
||||
$translations = [
|
||||
'write' => lang('Common.forms.editor.write'),
|
||||
'preview' => lang('Common.forms.editor.preview'),
|
||||
|
|
@ -85,19 +92,19 @@ class MarkdownEditor extends FormComponent
|
|||
$toolbarContent .= '<div class="inline-flex text-2xl gap-x-1">';
|
||||
foreach ($buttonsGroup as $button) {
|
||||
if (! in_array($button['name'], $this->disallowList, true)) {
|
||||
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
|
||||
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
|
||||
}
|
||||
}
|
||||
$toolbarContent .= '</div>';
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$editorClass}">
|
||||
<div class="{$wrapperClass}">
|
||||
<header class="px-2">
|
||||
<div class="sticky top-0 z-20 flex flex-wrap justify-between border-b border-gray-300 bg-elevated">
|
||||
<markdown-write-preview for="{$this->id}" class="relative inline-flex h-8">
|
||||
<button type="button" slot="write" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['write']}</button>
|
||||
<button type="button" slot="preview" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['preview']}</button>
|
||||
<button type="button" slot="write" class="px-2 font-semibold">{$translations['write']}</button>
|
||||
<button type="button" slot="preview" class="px-2 font-semibold">{$translations['preview']}</button>
|
||||
</markdown-write-preview>
|
||||
<markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">{$toolbarContent}</markdown-toolbar>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class MultiSelect extends FormComponent
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $selected = [];
|
||||
|
||||
public function setOptions(string $value): void
|
||||
{
|
||||
$this->options = json_decode(htmlspecialchars_decode($value), true);
|
||||
}
|
||||
|
||||
public function setSelected(string $selected): void
|
||||
{
|
||||
$this->selected = json_decode(htmlspecialchars_decode($selected), true);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$defaultAttributes = [
|
||||
'data-class' => $this->attributes['class'],
|
||||
'multiple' => 'multiple',
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
$this->attributes['class'] .= ' w-full bg-elevated border-3 border-contrast rounded-lg';
|
||||
$extra = array_merge($defaultAttributes, $this->attributes);
|
||||
|
||||
return form_dropdown($this->name, $this->options, $this->selected, $extra);
|
||||
}
|
||||
}
|
||||
41
app/Views/Components/Forms/PermalinkEditor.php
Normal file
41
app/Views/Components/Forms/PermalinkEditor.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class PermalinkEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['label', 'prefix', 'permalinkBase'];
|
||||
|
||||
protected string $label = '';
|
||||
|
||||
protected string $prefix = '';
|
||||
|
||||
protected string $permalinkBase = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('flex-1 text-xs border-contrast rounded-lg focus:border-contrast border-3 focus-within:ring-accent transition');
|
||||
|
||||
$this->attributes['slot'] = 'slug-input';
|
||||
$input = form_input($this->attributes, $this->getValue());
|
||||
|
||||
$editLabel = lang('Common.edit');
|
||||
$copyLabel = lang('Common.copy');
|
||||
$copiedLabel = lang('Common.copied');
|
||||
|
||||
return <<<HTML
|
||||
<div>
|
||||
<x-Forms.Label for="{$this->id}">{$this->label}</x-Forms.Label>
|
||||
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="{$editLabel}" copy-label="{$copyLabel}" copied-label="{$copiedLabel}" permalink-base="{$this->permalinkBase}">
|
||||
<span slot="domain">{$this->prefix}</span>
|
||||
{$input}
|
||||
</permalink-edit>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,29 +4,35 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class Radio extends FormComponent
|
||||
{
|
||||
protected array $props = ['isChecked'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$radioInput = form_radio(
|
||||
[
|
||||
'id' => $this->value,
|
||||
'name' => $this->name,
|
||||
'class' => 'text-accent-base bg-elevated border-contrast border-3 focus:ring-accent w-6 h-6',
|
||||
'class' => 'text-accent-base bg-elevated border-contrast border-3 focus:ring-accent w-6 h-6 transition',
|
||||
],
|
||||
$this->value,
|
||||
$this->getValue(),
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
);
|
||||
|
||||
$this->mergeClass('inline-flex items-center');
|
||||
|
||||
return <<<HTML
|
||||
<label class="inline-flex items-center {$this->class}">{$radioInput}<span class="ml-2">{$this->slot}</span></label>
|
||||
<label {$this->getStringifiedAttributes()}>{$radioInput}<span class="ml-2">{$this->slot}</span></label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,21 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class RadioButton extends FormComponent
|
||||
{
|
||||
protected bool $isChecked = false;
|
||||
protected array $props = ['isSelected', 'description'];
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected array $casts = [
|
||||
'isSelected' => 'boolean',
|
||||
];
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
protected bool $isSelected = false;
|
||||
|
||||
protected string $description = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$data = [
|
||||
|
|
@ -23,22 +27,34 @@ class RadioButton extends FormComponent
|
|||
'class' => 'form-radio-btn bg-elevated',
|
||||
];
|
||||
|
||||
if ($this->required) {
|
||||
if ($this->isRequired) {
|
||||
$data['required'] = 'required';
|
||||
}
|
||||
|
||||
$this->mergeClass('relative w-full');
|
||||
|
||||
$descriptionText = '';
|
||||
if ($this->description !== '') {
|
||||
$describerId = $this->name . 'Help';
|
||||
$descriptionText = <<<HTML
|
||||
<span id="{$describerId}" class="form-radio-btn-description">{$this->description}</span>
|
||||
HTML;
|
||||
$data['aria-describedby'] = $describerId;
|
||||
}
|
||||
|
||||
$radioInput = form_radio(
|
||||
$data,
|
||||
$this->value,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
$this->getValue(),
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||
);
|
||||
|
||||
$hint = $this->hint ? hint_tooltip($this->hint, 'ml-1 text-base') : '';
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$this->class}">
|
||||
<div {$this->getStringifiedAttributes()}">
|
||||
{$radioInput}
|
||||
<label for="{$this->value}">{$this->slot}{$hint}</label>
|
||||
<label for="{$this->value}">
|
||||
<span>{$this->slot}</span>
|
||||
{$descriptionText}
|
||||
</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
|
|
|||
70
app/Views/Components/Forms/RadioGroup.php
Normal file
70
app/Views/Components/Forms/RadioGroup.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
use Override;
|
||||
|
||||
class RadioGroup extends FormComponent
|
||||
{
|
||||
protected array $props = ['label', 'options', 'hint', 'helper'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
protected string $label;
|
||||
|
||||
/**
|
||||
* @var array{value:string,label:string,hint?:string}
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected string $helper = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('flex flex-col');
|
||||
$options = '';
|
||||
foreach ($this->options as $option) {
|
||||
$radioButtonData = [
|
||||
'value' => $option['value'],
|
||||
'name' => $this->name,
|
||||
'slot' => $option['label'],
|
||||
'description' => $option['description'] ?? '',
|
||||
'isSelected' => var_export($this->getValue() === '' ? $option['value'] === $this->options[0]['value'] : $option['value'] === $this->getValue(), true),
|
||||
'isRequired' => var_export($this->isRequired, true),
|
||||
];
|
||||
|
||||
$options .= new RadioButton($radioButtonData)->render();
|
||||
}
|
||||
|
||||
$helperText = '';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
])->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
$hint = $this->hint === '' ? '' : new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
])->render();
|
||||
|
||||
return <<<HTML
|
||||
<fieldset {$this->getStringifiedAttributes()}>
|
||||
<legend class="-mb-1 text-sm font-semibold">{$this->label}{$hint}</legend>
|
||||
{$helperText}
|
||||
<div class="grid grid-cols-radioGroup gap-2 mt-1">{$options}</div>
|
||||
</fieldset>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,27 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Section extends Component
|
||||
{
|
||||
protected string $title = '';
|
||||
protected array $props = ['title', 'subtitle'];
|
||||
|
||||
protected ?string $subtitle = null;
|
||||
protected string $title;
|
||||
|
||||
protected string $subtitle = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$subtitle = $this->subtitle === null ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
$subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
|
||||
$this->mergeClass('w-full p-4 sm:p-6 md:p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
|
||||
|
||||
return <<<HTML
|
||||
<fieldset class="w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl {$this->class}">
|
||||
<Heading tagName="legend" class="float-left">{$this->title}</Heading>
|
||||
<fieldset {$this->getStringifiedAttributes()}>
|
||||
<x-Heading tagName="legend" class="float-left">{$this->title}</x-Heading>
|
||||
{$subtitle}
|
||||
<div class="flex flex-col w-0 min-w-full gap-4 py-4">{$this->slot}</div>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -4,36 +4,44 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class Select extends FormComponent
|
||||
{
|
||||
protected array $props = ['options'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
* @var array<array<string, string>>
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
protected string $selected = '';
|
||||
|
||||
public function setOptions(string $value): void
|
||||
{
|
||||
$this->options = json_decode(htmlspecialchars_decode($value), true);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full focus:border-contrast border-3 rounded-lg bg-elevated border-contrast');
|
||||
$defaultAttributes = [
|
||||
'class' => 'w-full focus:border-contrast focus:ring-accent border-3 rounded-lg bg-elevated border-contrast ' . $this->class,
|
||||
'data-class' => $this->class,
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
unset($this->attributes['name']);
|
||||
unset($this->attributes['options']);
|
||||
unset($this->attributes['selected']);
|
||||
$extra = [...$this->attributes, ...$defaultAttributes];
|
||||
$this->attributes = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra);
|
||||
$options = '';
|
||||
$selected = $this->getValue();
|
||||
foreach ($this->options as $option) {
|
||||
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . ($option['value'] === $selected ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||
}
|
||||
|
||||
$this->attributes['name'] = $this->name;
|
||||
|
||||
return <<<HTML
|
||||
<select {$this->getStringifiedAttributes()}>{$options}</select>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
app/Views/Components/Forms/SelectMulti.php
Normal file
52
app/Views/Components/Forms/SelectMulti.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class SelectMulti extends FormComponent
|
||||
{
|
||||
protected array $props = ['options'];
|
||||
|
||||
protected array $casts = [
|
||||
'value' => 'array',
|
||||
'defaultValue' => 'array',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<array<string, string>>
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg relative');
|
||||
|
||||
$defaultAttributes = [
|
||||
'multiple' => 'multiple',
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
|
||||
$this->attributes = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
$options = '';
|
||||
$selected = $this->getValue();
|
||||
foreach ($this->options as $option) {
|
||||
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||
}
|
||||
|
||||
$this->attributes['name'] = $this->name . '[]';
|
||||
|
||||
return <<<HTML
|
||||
<select {$this->getStringifiedAttributes()}>{$options}</select>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,25 +4,27 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class Textarea extends FormComponent
|
||||
{
|
||||
public function setValue(?string $value): void
|
||||
protected array $attributes = [
|
||||
'rows' => '6',
|
||||
];
|
||||
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
if ($value) {
|
||||
$this->value = htmlspecialchars_decode($value);
|
||||
}
|
||||
$this->value = htmlspecialchars_decode($value);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
unset($this->attributes['value']);
|
||||
$this->mergeClass('bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent transition');
|
||||
|
||||
$this->attributes['class'] = 'bg-elevated w-full focus:border-contrast focus:ring-accent rounded-lg border-3 border-contrast ' . $this->class;
|
||||
$this->attributes['id'] = $this->id;
|
||||
|
||||
$textarea = form_textarea(
|
||||
$this->attributes,
|
||||
old($this->name, $this->value ?? '', false)
|
||||
);
|
||||
$textarea = form_textarea($this->attributes, $this->getValue());
|
||||
|
||||
return <<<HTML
|
||||
{$textarea}
|
||||
|
|
|
|||
|
|
@ -4,45 +4,62 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
use Override;
|
||||
|
||||
class Toggler extends FormComponent
|
||||
{
|
||||
/**
|
||||
* @var 'base'|'small
|
||||
*/
|
||||
protected string $size = 'base';
|
||||
protected array $props = ['size', 'hint', 'helper'];
|
||||
|
||||
protected string $label = '';
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $checked = false;
|
||||
protected string $helper = '';
|
||||
|
||||
public function setChecked(string $value): void
|
||||
{
|
||||
$this->checked = $value === 'true';
|
||||
}
|
||||
protected bool $isChecked = false;
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
unset($this->attributes['checked']);
|
||||
$this->mergeClass('relative justify-between inline-flex items-start gap-x-2');
|
||||
|
||||
$wrapperClass = $this->class;
|
||||
unset($this->attributes['class']);
|
||||
$checkbox = form_checkbox(
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-switch',
|
||||
],
|
||||
'yes',
|
||||
in_array($this->getValue(), ['yes', 'true', 'on', '1'], true),
|
||||
);
|
||||
|
||||
$sizeClass = [
|
||||
'base' => 'form-switch-slider',
|
||||
'small' => 'form-switch-slider form-switch-slider--small',
|
||||
];
|
||||
$hint = $this->hint === '' ? '' : new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
])->render();
|
||||
|
||||
$this->attributes['class'] = 'form-switch';
|
||||
$helperText = '';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
'class' => '-mt-1',
|
||||
])->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
$checkbox = form_checkbox($this->attributes, $this->value, old($this->name) === 'yes' ? true : $this->checked);
|
||||
$hint = $this->hint === '' ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
return <<<HTML
|
||||
<label class="relative inline-flex items-center {$wrapperClass}">
|
||||
<label {$this->getStringifiedAttributes()}>
|
||||
<div class="flex flex-col">
|
||||
<span>{$this->slot}{$hint}</span>
|
||||
{$helperText}
|
||||
</div>
|
||||
{$checkbox}
|
||||
<span class="{$sizeClass[$this->size]}"></span>
|
||||
<span class="ml-2">{$this->slot}{$hint}</span>
|
||||
<span class="form-switch-slider"></span>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class XMLEditor extends FormComponent
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $attributes = [
|
||||
'rows' => '5',
|
||||
'class' => 'textarea',
|
||||
];
|
||||
|
||||
protected string $content = '';
|
||||
|
||||
public function setContent(string $value): void
|
||||
{
|
||||
$this->content = htmlspecialchars_decode($value);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['slot'] = 'textarea';
|
||||
$textarea = form_textarea($this->attributes, $this->content);
|
||||
|
||||
return <<<HTML
|
||||
<xml-editor>{$textarea}</time-ago>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Heading extends Component
|
||||
{
|
||||
protected array $props = ['tagName', 'size'];
|
||||
|
||||
protected string $tagName = 'div';
|
||||
|
||||
/**
|
||||
|
|
@ -15,18 +18,20 @@ class Heading extends Component
|
|||
*/
|
||||
protected string $size = 'base';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$sizeClasses = [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'tracking-wide text-base',
|
||||
'base' => 'text-xl',
|
||||
'large' => 'text-3xl',
|
||||
];
|
||||
default => 'text-xl',
|
||||
};
|
||||
|
||||
$class = $this->class . ' relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] ' . $sizeClasses[$this->size];
|
||||
$this->mergeClass('relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10]');
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
return <<<HTML
|
||||
<{$this->tagName} class="{$class}">{$this->slot}</{$this->tagName}>
|
||||
<{$this->tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$this->tagName}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
app/Views/Components/Hint.php
Normal file
30
app/Views/Components/Hint.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Hint extends Component
|
||||
{
|
||||
protected array $attributes = [
|
||||
'data-tooltip' => 'bottom',
|
||||
'tabindex' => '0',
|
||||
];
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['title'] = $this->slot;
|
||||
|
||||
$this->mergeClass('inline-block align-middle opacity-75');
|
||||
|
||||
$icon = icon('question-fill');
|
||||
|
||||
return <<<HTML
|
||||
<span {$this->getStringifiedAttributes()}>{$icon}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,9 @@ namespace App\Views\Components;
|
|||
|
||||
class IconButton extends Button
|
||||
{
|
||||
public string $glyph = '';
|
||||
protected array $props = ['glyph'];
|
||||
|
||||
protected string $glyph;
|
||||
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
|
|
@ -16,18 +18,18 @@ class IconButton extends Button
|
|||
'data-tooltip' => 'bottom',
|
||||
];
|
||||
|
||||
$glyphSize = [
|
||||
'small' => 'text-sm',
|
||||
'base' => 'text-lg',
|
||||
'large' => 'text-2xl',
|
||||
];
|
||||
|
||||
$allAttributes = [...$attributes, ...$iconButtonAttributes];
|
||||
|
||||
parent::__construct($allAttributes);
|
||||
|
||||
$glyphSizeClass = match ($this->size) {
|
||||
'small' => 'text-sm',
|
||||
'large' => 'text-2xl',
|
||||
default => 'text-lg',
|
||||
};
|
||||
|
||||
$this->slot = (string) icon($this->glyph, [
|
||||
'class' => $glyphSize[$this->size],
|
||||
'class' => $glyphSizeClass,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,57 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Pill extends Component
|
||||
{
|
||||
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
|
||||
|
||||
/**
|
||||
* @var 'small'|'base'
|
||||
*/
|
||||
public string $size = 'base';
|
||||
protected string $size = 'base';
|
||||
|
||||
public string $variant = 'default';
|
||||
protected string $variant = 'default';
|
||||
|
||||
public ?string $icon = null;
|
||||
protected string $icon = '';
|
||||
|
||||
public ?string $iconClass = '';
|
||||
protected string $iconClass = '';
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected string $hint = '';
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$variantClasses = [
|
||||
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
$variantClass = match ($this->variant) {
|
||||
'primary' => 'text-accent-contrast bg-accent-base border-accent-base',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'danger' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
];
|
||||
default => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
};
|
||||
|
||||
$icon = $this->icon ? icon($this->icon, [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'text-xs tracking-wide',
|
||||
default => 'text-sm',
|
||||
};
|
||||
|
||||
$icon = $this->icon !== '' ? icon($this->icon, [
|
||||
'class' => $this->iconClass,
|
||||
]) : '';
|
||||
$hint = $this->hint ? 'data-tooltip="bottom" title="' . $this->hint . '"' : '';
|
||||
|
||||
if ($this->hint !== '') {
|
||||
$this->attributes['data-tooltip'] = 'bottom';
|
||||
$this->attributes['title'] = $this->hint;
|
||||
}
|
||||
|
||||
$this->mergeClass('inline-flex lowercase items-center gap-x-1 px-1 font-semibold border rounded');
|
||||
$this->mergeClass($variantClass);
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
return <<<HTML
|
||||
<span class="inline-flex items-center gap-x-1 px-1 font-semibold text-sm border rounded {$variantClasses[$this->variant]} {$this->class}" {$hint}>{$icon}{$this->slot}</span>
|
||||
<span {$this->getStringifiedAttributes()}>{$icon}{$this->slot}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,29 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class ReadMore extends Component
|
||||
{
|
||||
public string $id;
|
||||
protected array $props = ['id'];
|
||||
|
||||
protected string $id;
|
||||
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$readMoreLabel = lang('Common.read_more');
|
||||
$readLessLabel = lang('Common.read_less');
|
||||
|
||||
$this->mergeClass('read-more');
|
||||
$this->attributes['style'] = '--line-clamp: 3';
|
||||
|
||||
return <<<HTML
|
||||
<div class="read-more {$this->class}" style="--line-clamp: 3">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<input id="read-more-checkbox_{$this->id}" type="checkbox" class="read-more__checkbox" aria-hidden="true">
|
||||
<div class="mb-2 read-more__text">{$this->slot}</div>
|
||||
<label for="read-more-checkbox_{$this->id}" class="read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
|
||||
<div class="read-more__text">{$this->slot}</div>
|
||||
<label for="read-more-checkbox_{$this->id}" class="mt-2 read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,25 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use Override;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class SeeMore extends Component
|
||||
{
|
||||
#[Override]
|
||||
public function render(): string
|
||||
{
|
||||
$seeMoreLabel = lang('Common.see_more');
|
||||
$seeLessLabel = lang('Common.see_less');
|
||||
|
||||
$this->mergeClass('see-more');
|
||||
$this->attributes['styles'] = '--content-height: 10rem';
|
||||
|
||||
return <<<HTML
|
||||
<div class="see-more" style="--content-height: 10rem">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<input id="see-more-checkbox" type="checkbox" class="see-more__checkbox" aria-hidden="true">
|
||||
<div class="mb-2 see-more__content {$this->class}"><div class="see-more_content-fade"></div>{$this->slot}</div>
|
||||
<label for="see-more-checkbox" class="see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
|
||||
<div class="see-more__content"><div class="see-more_content-fade"></div>{$this->slot}</div>
|
||||
<label for="see-more-checkbox" class="mt-2 see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
if (session()->has('message')): ?>
|
||||
<Alert variant="success" class="mb-4"><?= esc(session('message')) ?></Alert>
|
||||
<Alert variant="success" class="mb-4"><?= session('message') ?></Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('error')): ?>
|
||||
<Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></Alert>
|
||||
<Alert variant="danger" class="mb-4"><?= session('error') ?></Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('errors')): ?>
|
||||
<Alert variant="danger" class="mb-4">
|
||||
<ul>
|
||||
<?php foreach (session('errors') as $error): ?>
|
||||
<li><?= esc($error) ?></li>
|
||||
<li><?= $error ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ p.lead {
|
|||
.header {
|
||||
background: var(--light-bg-color);
|
||||
color: var(--dark-text-color);
|
||||
margin-top: 2.17rem;
|
||||
}
|
||||
|
||||
.header .container {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,84 @@
|
|||
<?= helper(['components', 'svg']) ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title><?= lang('Errors.badRequest') ?></title>
|
||||
|
||||
<title><?= lang('Errors.pageNotFound') ?></title>
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')->asset('styles/index.css') ?>
|
||||
<style>
|
||||
div.logo {
|
||||
height: 200px;
|
||||
width: 155px;
|
||||
display: inline-block;
|
||||
opacity: 0.08;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
left: 50%;
|
||||
margin-left: -73px;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #777;
|
||||
font-weight: 300;
|
||||
}
|
||||
h1 {
|
||||
font-weight: lighter;
|
||||
letter-spacing: normal;
|
||||
font-size: 3rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: #222;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1024px;
|
||||
margin: 5rem auto;
|
||||
padding: 2rem;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
pre {
|
||||
white-space: normal;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
code {
|
||||
background: #fafafa;
|
||||
border: 1px solid #efefef;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid #efefef;
|
||||
padding: 1em 2em 0 2em;
|
||||
font-size: 85%;
|
||||
color: #999;
|
||||
}
|
||||
a:active,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: #dd4814;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>400</h1>
|
||||
|
||||
<body class="flex flex-col items-center justify-center min-h-screen px-2 text-center bg-base theme-<?= service('settings')
|
||||
->get('App.theme') ?>">
|
||||
<?= svg('castopod-mascot_confused', 'h-64') ?>
|
||||
<h1 class="mt-4 text-3xl font-bold font-display md:text-4xl lg:text-5xl">400</h1>
|
||||
|
||||
<p class="mb-6 text-lg text-skin-muted md:text-xl lg:text-2xl">
|
||||
<p>
|
||||
<?php if (ENVIRONMENT !== 'production') : ?>
|
||||
<?= nl2br(esc($message)) ?>
|
||||
<?php else : ?>
|
||||
<?= lang('Errors.sorryBadRequest') ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?= helper(['components', 'svg']) ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="<?= service('request')
|
||||
->getLocale() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
|
||||
<title>403 Forbidden</title>
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')->asset('styles/index.css') ?>
|
||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center justify-center min-h-screen px-2 text-center bg-base theme-<?= service('settings')
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
You do not have sufficient permissions to access that page.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?= helper(['components', 'svg']) ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="<?= service('request')
|
||||
->getLocale() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
|
||||
<title><?= lang('Errors.pageNotFound') ?></title>
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')->asset('styles/index.css') ?>
|
||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center justify-center min-h-screen px-2 text-center bg-base theme-<?= service('settings')
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
<?= lang('Errors.sorryCannotFind') ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
|
||||
<title><?= lang('Errors.whoops') ?></title>
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')->asset('styles/index.css') ?>
|
||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||
<?php if (auth()->loggedIn()): ?>
|
||||
<?= service('vite')->asset('js/error.ts') ?>
|
||||
<?= service('vite')->asset('js/error.ts', 'js') ?>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<h2 class="font-mono font-semibold"><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h2>
|
||||
<p class="font-mono"><?= nl2br(esc($exception->getMessage())) ?><br/><span class="pl-4">at <span class="select-all bg-elevated"><?= nl2br(esc($exception->getFile())) ?>:<?= esc($exception->getLine()) ?></span></span></p>
|
||||
<p id="error-stack-trace" class="hidden"><?= nl2br(esc($exception)) ?></p>
|
||||
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base focus:ring-accent">
|
||||
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base">
|
||||
<span class="inline-flex items-center copy-base"><?= icon('file-copy-fill', [
|
||||
'class' => 'mr-2',
|
||||
]) ?>Copy stack trace</span>
|
||||
|
|
@ -41,11 +41,11 @@
|
|||
<div class="flex flex-col justify-center w-full gap-6 py-12 border-t-2 md:flex-row border-subtle">
|
||||
<div class="w-full max-w-md mx-auto md:mx-0">
|
||||
<h2 class="text-xl font-semibold font-display">Found a bug?</h2>
|
||||
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline focus:ring-accent decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
|
||||
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
|
||||
</div>
|
||||
<div class="w-full max-w-md mx-auto md:mx-0">
|
||||
<h2 class="text-xl font-semibold font-display">Not sure what's happening?</h2>
|
||||
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline focus:ring-accent decoration-accent">Castopod community chat</a>!</p>
|
||||
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline decoration-accent">Castopod community chat</a>!</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
{
|
||||
"name": "adaures/castopod",
|
||||
"version": "1.15.5",
|
||||
"version": "2.0.0-dev",
|
||||
"type": "project",
|
||||
"description": "Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
|
||||
"homepage": "https://castopod.org",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.5",
|
||||
"adaures/castopod-plugins-manager": "dev-main",
|
||||
"adaures/ipcat-php": "^v1.0.0",
|
||||
"adaures/podcast-persons-taxonomy": "^v1.0.1",
|
||||
"aws/aws-sdk-php": "^3.369.36",
|
||||
"aws/aws-sdk-php": "^3.369.37",
|
||||
"chrisjean/php-ico": "^1.0.4",
|
||||
"cocur/slugify": "^v4.7.1",
|
||||
"codeigniter4/framework": "4.6.5",
|
||||
"cocur/slugify": "4.7.1",
|
||||
"codeigniter4/framework": "4.7.0",
|
||||
"codeigniter4/settings": "v2.2.0",
|
||||
"codeigniter4/shield": "^1.2.0",
|
||||
"codeigniter4/shield": "1.2.0",
|
||||
"codeigniter4/tasks": "dev-develop",
|
||||
"geoip2/geoip2": "3.3.0",
|
||||
"james-heinrich/getid3": "^2.0.0-beta6",
|
||||
|
|
@ -25,19 +26,19 @@
|
|||
"mpratt/embera": "^2.0.42",
|
||||
"opawg/user-agents-v2-php": "dev-main",
|
||||
"phpseclib/phpseclib": "~2.0.51",
|
||||
"vlucas/phpdotenv": "^5.6.3",
|
||||
"vlucas/phpdotenv": "5.6.3",
|
||||
"whichbrowser/parser": "^v2.1.8",
|
||||
"yassinedoghri/codeigniter-vite": "^2.1",
|
||||
"yassinedoghri/php-icons": "^1.3.0",
|
||||
"yassinedoghri/codeigniter-vite": "^2.1.0",
|
||||
"yassinedoghri/php-icons": "1.3.0",
|
||||
"yassinedoghri/podcast-feed": "dev-main"
|
||||
},
|
||||
"require-dev": {
|
||||
"captainhook/captainhook": "^5.28.3",
|
||||
"codeigniter/phpstan-codeigniter": "^1.5.4",
|
||||
"mikey179/vfsstream": "v1.6.12",
|
||||
"codeigniter/phpstan-codeigniter": "1.5.4",
|
||||
"mikey179/vfsstream": "^v1.6.12",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^2.1.39",
|
||||
"phpunit/phpunit": "^10.5.63",
|
||||
"phpunit/phpunit": "^13.0.5",
|
||||
"rector/rector": "^2.3.6",
|
||||
"symplify/coding-standard": "^13.0.0",
|
||||
"symplify/easy-coding-standard": "^13.0.4"
|
||||
|
|
|
|||
1098
composer.lock
generated
1098
composer.lock
generated
|
|
@ -4,8 +4,63 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ae175922494cbac88f4477946a143ae8",
|
||||
"content-hash": "de1c665976cbb37fec3de326336d83f5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adaures/castopod-plugins-manager",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ad-aures/castopod-plugins-manager.git",
|
||||
"reference": "53430f9a57cd38eee3e3dfe5953764cc42c2a0c9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ad-aures/castopod-plugins-manager/zipball/53430f9a57cd38eee3e3dfe5953764cc42c2a0c9",
|
||||
"reference": "53430f9a57cd38eee3e3dfe5953764cc42c2a0c9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"z4kn4fein/php-semver": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^4.0.4",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0.2",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^2.1.22",
|
||||
"rector/rector": "^2.1.4",
|
||||
"symplify/coding-standard": "^12.4.3",
|
||||
"symplify/easy-coding-standard": "^12.5.24"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Constants.php",
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Castopod\\PluginsManager\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"AGPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Yassine Doghri",
|
||||
"homepage": "https://yassinedoghri.com/"
|
||||
}
|
||||
],
|
||||
"description": "A PHP library to install, update, and remove plugins on a Castopod instance.",
|
||||
"support": {
|
||||
"issues": "https://github.com/ad-aures/castopod-plugins-manager/issues",
|
||||
"source": "https://github.com/ad-aures/castopod-plugins-manager/tree/main"
|
||||
},
|
||||
"time": "2025-10-06T15:58:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "adaures/ipcat-php",
|
||||
"version": "v1.0.0",
|
||||
|
|
@ -206,16 +261,16 @@
|
|||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.369.36",
|
||||
"version": "3.369.37",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0"
|
||||
"reference": "bc599ce989b101ee630d5e1a1aeda387632df47b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0",
|
||||
"reference": "2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bc599ce989b101ee630d5e1a1aeda387632df47b",
|
||||
"reference": "bc599ce989b101ee630d5e1a1aeda387632df47b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -297,9 +352,9 @@
|
|||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.36"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.37"
|
||||
},
|
||||
"time": "2026-02-17T19:45:01+00:00"
|
||||
"time": "2026-02-18T19:16:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
@ -484,23 +539,23 @@
|
|||
},
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
"version": "v4.6.5",
|
||||
"version": "v4.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/codeigniter4/framework.git",
|
||||
"reference": "116e0919590a412c09d2b9e4f6b8addda18224d8"
|
||||
"reference": "e7753bc03f8b74af428f46b5e2bb74925487c930"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/codeigniter4/framework/zipball/116e0919590a412c09d2b9e4f6b8addda18224d8",
|
||||
"reference": "116e0919590a412c09d2b9e4f6b8addda18224d8",
|
||||
"url": "https://api.github.com/repos/codeigniter4/framework/zipball/e7753bc03f8b74af428f46b5e2bb74925487c930",
|
||||
"reference": "e7753bc03f8b74af428f46b5e2bb74925487c930",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-intl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"laminas/laminas-escaper": "^2.17",
|
||||
"php": "^8.1",
|
||||
"laminas/laminas-escaper": "^2.18",
|
||||
"php": "^8.2",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
@ -514,6 +569,7 @@
|
|||
"predis/predis": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-apcu": "If you use Cache class ApcuHandler",
|
||||
"ext-curl": "If you use CURLRequest class",
|
||||
"ext-dom": "If you use TestResponse",
|
||||
"ext-exif": "If you run Image class tests",
|
||||
|
|
@ -525,7 +581,9 @@
|
|||
"ext-memcached": "If you use Cache class MemcachedHandler with Memcached",
|
||||
"ext-mysqli": "If you use MySQL",
|
||||
"ext-oci8": "If you use Oracle Database",
|
||||
"ext-pcntl": "If you use Signals",
|
||||
"ext-pgsql": "If you use PostgreSQL",
|
||||
"ext-posix": "If you use Signals",
|
||||
"ext-readline": "Improves CLI::input() usability",
|
||||
"ext-redis": "If you use Cache class RedisHandler",
|
||||
"ext-simplexml": "If you format XML",
|
||||
|
|
@ -554,7 +612,7 @@
|
|||
"slack": "https://codeigniterchat.slack.com",
|
||||
"source": "https://github.com/codeigniter4/CodeIgniter4"
|
||||
},
|
||||
"time": "2026-02-01T17:59:34+00:00"
|
||||
"time": "2026-02-01T20:39:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "codeigniter4/queue",
|
||||
|
|
@ -3201,25 +3259,25 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.0",
|
||||
"version": "v8.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d551b38811096d0be9c4691d406991b47c0c630a"
|
||||
"reference": "d937d400b980523dc9ee946bb69972b5e619058d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
|
||||
"reference": "d551b38811096d0be9c4691d406991b47c0c630a",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
|
||||
"reference": "d937d400b980523dc9ee946bb69972b5e619058d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
"symfony/process": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -3247,7 +3305,7 @@
|
|||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.0"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3267,7 +3325,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-27T13:27:24+00:00"
|
||||
"time": "2025-12-01T09:13:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
|
@ -3836,6 +3894,60 @@
|
|||
"source": "https://github.com/yassinedoghri/podcast-feed/tree/main"
|
||||
},
|
||||
"time": "2024-04-28T16:17:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "z4kn4fein/php-semver",
|
||||
"version": "v3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/z4kn4fein/php-semver.git",
|
||||
"reference": "049a1d81e92235c8b3c9ab30a96fcbaa929a266d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/z4kn4fein/php-semver/zipball/049a1d81e92235c8b3c9ab30a96fcbaa929a266d",
|
||||
"reference": "049a1d81e92235c8b3c9ab30a96fcbaa929a266d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^10"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"z4kn4fein\\SemVer\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Peter Csajtai",
|
||||
"email": "peter.csajtai@outlook.com"
|
||||
}
|
||||
],
|
||||
"description": "Semantic Versioning library for PHP. It implements the full semantic version 2.0.0 specification and provides ability to parse, compare, and increment semantic versions along with validation against constraints.",
|
||||
"homepage": "https://github.com/z4kn4fein/php-semver",
|
||||
"keywords": [
|
||||
"comparison",
|
||||
"semantic",
|
||||
"semver",
|
||||
"validation",
|
||||
"version",
|
||||
"versioning"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/z4kn4fein/php-semver/issues",
|
||||
"source": "https://github.com/z4kn4fein/php-semver/tree/v3.0.0"
|
||||
},
|
||||
"time": "2024-04-01T16:17:27+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
|
@ -4940,35 +5052,34 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "10.1.16",
|
||||
"version": "13.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "7e308268858ed6baedc8704a304727d20bc07c77"
|
||||
"reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
|
||||
"reference": "7e308268858ed6baedc8704a304727d20bc07c77",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/a8b58fde2f4fbc69a064e1f80ff917607cf7737c",
|
||||
"reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^4.19.1 || ^5.1.0",
|
||||
"php": ">=8.1",
|
||||
"phpunit/php-file-iterator": "^4.1.0",
|
||||
"phpunit/php-text-template": "^3.0.1",
|
||||
"sebastian/code-unit-reverse-lookup": "^3.0.0",
|
||||
"sebastian/complexity": "^3.2.0",
|
||||
"sebastian/environment": "^6.1.0",
|
||||
"sebastian/lines-of-code": "^2.0.2",
|
||||
"sebastian/version": "^4.0.1",
|
||||
"theseer/tokenizer": "^1.2.3"
|
||||
"nikic/php-parser": "^5.7.0",
|
||||
"php": ">=8.4",
|
||||
"phpunit/php-file-iterator": "^7.0",
|
||||
"phpunit/php-text-template": "^6.0",
|
||||
"sebastian/complexity": "^6.0",
|
||||
"sebastian/environment": "^9.0",
|
||||
"sebastian/lines-of-code": "^5.0",
|
||||
"sebastian/version": "^7.0",
|
||||
"theseer/tokenizer": "^2.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcov": "PHP extension that provides line coverage",
|
||||
|
|
@ -4977,7 +5088,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "10.1.x-dev"
|
||||
"dev-main": "13.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5006,40 +5117,52 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/13.0.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-22T04:31:57+00:00"
|
||||
"time": "2026-02-06T06:05:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
"version": "4.1.0",
|
||||
"version": "7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
||||
"reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
|
||||
"reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
|
||||
"reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50",
|
||||
"reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "4.0-dev"
|
||||
"dev-main": "7.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5067,36 +5190,48 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-31T06:24:48+00:00"
|
||||
"time": "2026-02-06T04:33:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-invoker",
|
||||
"version": "4.0.0",
|
||||
"version": "7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-invoker.git",
|
||||
"reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
|
||||
"reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
|
||||
"reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88",
|
||||
"reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-pcntl": "*",
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "*"
|
||||
|
|
@ -5104,7 +5239,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "4.0-dev"
|
||||
"dev-main": "7.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5130,40 +5265,53 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-invoker/issues",
|
||||
"source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
|
||||
"security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T06:56:09+00:00"
|
||||
"time": "2026-02-06T04:34:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-text-template",
|
||||
"version": "3.0.1",
|
||||
"version": "6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-text-template.git",
|
||||
"reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
|
||||
"reference": "a47af19f93f76aa3368303d752aa5272ca3299f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
|
||||
"reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4",
|
||||
"reference": "a47af19f93f76aa3368303d752aa5272ca3299f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.0-dev"
|
||||
"dev-main": "6.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5190,40 +5338,52 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-31T14:07:24+00:00"
|
||||
"time": "2026-02-06T04:36:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-timer",
|
||||
"version": "6.0.0",
|
||||
"version": "9.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-timer.git",
|
||||
"reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
|
||||
"reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
|
||||
"reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6",
|
||||
"reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "6.0-dev"
|
||||
"dev-main": "9.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5249,28 +5409,41 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
|
||||
"source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
|
||||
"security": "https://github.com/sebastianbergmann/php-timer/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T06:57:52+00:00"
|
||||
"time": "2026-02-06T04:37:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.63",
|
||||
"version": "13.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "33198268dad71e926626b618f3ec3966661e4d90"
|
||||
"reference": "d57826e8921a534680c613924bfd921ded8047f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90",
|
||||
"reference": "33198268dad71e926626b618f3ec3966661e4d90",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d57826e8921a534680c613924bfd921ded8047f4",
|
||||
"reference": "d57826e8921a534680c613924bfd921ded8047f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -5283,26 +5456,23 @@
|
|||
"myclabs/deep-copy": "^1.13.4",
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.1",
|
||||
"phpunit/php-code-coverage": "^10.1.16",
|
||||
"phpunit/php-file-iterator": "^4.1.0",
|
||||
"phpunit/php-invoker": "^4.0.0",
|
||||
"phpunit/php-text-template": "^3.0.1",
|
||||
"phpunit/php-timer": "^6.0.0",
|
||||
"sebastian/cli-parser": "^2.0.1",
|
||||
"sebastian/code-unit": "^2.0.0",
|
||||
"sebastian/comparator": "^5.0.5",
|
||||
"sebastian/diff": "^5.1.1",
|
||||
"sebastian/environment": "^6.1.0",
|
||||
"sebastian/exporter": "^5.1.4",
|
||||
"sebastian/global-state": "^6.0.2",
|
||||
"sebastian/object-enumerator": "^5.0.0",
|
||||
"sebastian/recursion-context": "^5.0.1",
|
||||
"sebastian/type": "^4.0.0",
|
||||
"sebastian/version": "^4.0.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-soap": "To be able to generate mocks based on WSDL files"
|
||||
"php": ">=8.4.1",
|
||||
"phpunit/php-code-coverage": "^13.0.1",
|
||||
"phpunit/php-file-iterator": "^7.0.0",
|
||||
"phpunit/php-invoker": "^7.0.0",
|
||||
"phpunit/php-text-template": "^6.0.0",
|
||||
"phpunit/php-timer": "^9.0.0",
|
||||
"sebastian/cli-parser": "^5.0.0",
|
||||
"sebastian/comparator": "^8.0.0",
|
||||
"sebastian/diff": "^8.0.0",
|
||||
"sebastian/environment": "^9.0.0",
|
||||
"sebastian/exporter": "^8.0.0",
|
||||
"sebastian/global-state": "^9.0.0",
|
||||
"sebastian/object-enumerator": "^8.0.0",
|
||||
"sebastian/recursion-context": "^8.0.0",
|
||||
"sebastian/type": "^7.0.0",
|
||||
"sebastian/version": "^7.0.0",
|
||||
"staabm/side-effects-detector": "^1.0.5"
|
||||
},
|
||||
"bin": [
|
||||
"phpunit"
|
||||
|
|
@ -5310,7 +5480,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "10.5-dev"
|
||||
"dev-main": "13.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -5342,7 +5512,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/13.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -5366,7 +5536,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-27T05:48:37+00:00"
|
||||
"time": "2026-02-18T12:40:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
|
|
@ -6009,28 +6179,28 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
"version": "2.0.1",
|
||||
"version": "5.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/cli-parser.git",
|
||||
"reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
|
||||
"reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
|
||||
"reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca",
|
||||
"reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.0-dev"
|
||||
"dev-main": "5.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6054,155 +6224,59 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
|
||||
"security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-02T07:12:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/code-unit",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/code-unit.git",
|
||||
"reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
|
||||
"reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Collection of value objects that represent the PHP code units",
|
||||
"homepage": "https://github.com/sebastianbergmann/code-unit",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/code-unit/issues",
|
||||
"source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
|
||||
},
|
||||
"funding": [
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T06:58:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/code-unit-reverse-lookup",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
|
||||
"reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
|
||||
"reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"description": "Looks up which function or method a line of code belongs to",
|
||||
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
|
||||
"source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T06:59:15+00:00"
|
||||
"time": "2026-02-06T04:39:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "5.0.5",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d"
|
||||
"reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
|
||||
"reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/29b232ddc29c2b114c0358c69b3084e7c3da0d58",
|
||||
"reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.1",
|
||||
"sebastian/diff": "^5.0",
|
||||
"sebastian/exporter": "^5.0"
|
||||
"php": ">=8.4",
|
||||
"sebastian/diff": "^8.0",
|
||||
"sebastian/exporter": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "For comparing BcMath\\Number objects"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.0-dev"
|
||||
"dev-main": "8.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6242,7 +6316,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6262,33 +6336,33 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-24T09:25:16+00:00"
|
||||
"time": "2026-02-06T04:40:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
"version": "3.2.0",
|
||||
"version": "6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/complexity.git",
|
||||
"reference": "68ff824baeae169ec9f2137158ee529584553799"
|
||||
"reference": "c5651c795c98093480df79350cb050813fc7a2f3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
|
||||
"reference": "68ff824baeae169ec9f2137158ee529584553799",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3",
|
||||
"reference": "c5651c795c98093480df79350cb050813fc7a2f3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nikic/php-parser": "^4.18 || ^5.0",
|
||||
"php": ">=8.1"
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.2-dev"
|
||||
"dev-main": "6.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6312,41 +6386,53 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/complexity/issues",
|
||||
"security": "https://github.com/sebastianbergmann/complexity/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
|
||||
"source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/complexity",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-21T08:37:17+00:00"
|
||||
"time": "2026-02-06T04:41:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
"version": "5.1.1",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||
"reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
|
||||
"reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
|
||||
"reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2b6d09d7729ee87d605a439469f9dcc39be5ea3",
|
||||
"reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"symfony/process": "^6.4"
|
||||
"phpunit/phpunit": "^13.0",
|
||||
"symfony/process": "^7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.1-dev"
|
||||
"dev-main": "8.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6379,35 +6465,47 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
||||
"security": "https://github.com/sebastianbergmann/diff/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/diff",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-02T07:15:17+00:00"
|
||||
"time": "2026-02-06T04:42:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
"version": "6.1.0",
|
||||
"version": "9.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/environment.git",
|
||||
"reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
|
||||
"reference": "bb64d08145b021b67d5f253308a498b73ab0461e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
|
||||
"reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/bb64d08145b021b67d5f253308a498b73ab0461e",
|
||||
"reference": "bb64d08145b021b67d5f253308a498b73ab0461e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-posix": "*"
|
||||
|
|
@ -6415,7 +6513,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "6.1-dev"
|
||||
"dev-main": "9.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6443,42 +6541,54 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/environment/issues",
|
||||
"security": "https://github.com/sebastianbergmann/environment/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/9.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-23T08:47:14+00:00"
|
||||
"time": "2026-02-06T04:43:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
"version": "5.1.4",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/exporter.git",
|
||||
"reference": "0735b90f4da94969541dac1da743446e276defa6"
|
||||
"reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
|
||||
"reference": "0735b90f4da94969541dac1da743446e276defa6",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea",
|
||||
"reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.1",
|
||||
"sebastian/recursion-context": "^5.0"
|
||||
"php": ">=8.4",
|
||||
"sebastian/recursion-context": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.1-dev"
|
||||
"dev-main": "8.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6521,7 +6631,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/exporter/issues",
|
||||
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
|
||||
"source": "https://github.com/sebastianbergmann/exporter/tree/8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6541,35 +6651,35 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-24T06:09:11+00:00"
|
||||
"time": "2026-02-06T04:44:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/global-state",
|
||||
"version": "6.0.2",
|
||||
"version": "9.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/global-state.git",
|
||||
"reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
|
||||
"reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
|
||||
"reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e52e3dc22441e6218c710afe72c3042f8fc41ea7",
|
||||
"reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"sebastian/object-reflector": "^3.0",
|
||||
"sebastian/recursion-context": "^5.0"
|
||||
"php": ">=8.4",
|
||||
"sebastian/object-reflector": "^6.0",
|
||||
"sebastian/recursion-context": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-dom": "*",
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "6.0-dev"
|
||||
"dev-main": "9.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6595,41 +6705,53 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/global-state/issues",
|
||||
"security": "https://github.com/sebastianbergmann/global-state/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
|
||||
"source": "https://github.com/sebastianbergmann/global-state/tree/9.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-02T07:19:19+00:00"
|
||||
"time": "2026-02-06T04:45:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/lines-of-code",
|
||||
"version": "2.0.2",
|
||||
"version": "5.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/lines-of-code.git",
|
||||
"reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
|
||||
"reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
|
||||
"reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/4f21bb7768e1c997722ccc7efb1d6b5c11bfd471",
|
||||
"reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nikic/php-parser": "^4.18 || ^5.0",
|
||||
"php": ">=8.1"
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.0-dev"
|
||||
"dev-main": "5.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6653,42 +6775,54 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
|
||||
"security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
|
||||
"source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-21T08:38:20+00:00"
|
||||
"time": "2026-02-06T04:45:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/object-enumerator",
|
||||
"version": "5.0.0",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
|
||||
"reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
|
||||
"reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
|
||||
"reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5",
|
||||
"reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"sebastian/object-reflector": "^3.0",
|
||||
"sebastian/recursion-context": "^5.0"
|
||||
"php": ">=8.4",
|
||||
"sebastian/object-reflector": "^6.0",
|
||||
"sebastian/recursion-context": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.0-dev"
|
||||
"dev-main": "8.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6710,40 +6844,53 @@
|
|||
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
|
||||
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
|
||||
"security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T07:08:32+00:00"
|
||||
"time": "2026-02-06T04:46:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/object-reflector",
|
||||
"version": "3.0.0",
|
||||
"version": "6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/object-reflector.git",
|
||||
"reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
|
||||
"reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
|
||||
"reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200",
|
||||
"reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.0-dev"
|
||||
"dev-main": "6.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6765,40 +6912,53 @@
|
|||
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
|
||||
"source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
|
||||
"security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T07:06:18+00:00"
|
||||
"time": "2026-02-06T04:47:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/recursion-context",
|
||||
"version": "5.0.1",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/recursion-context.git",
|
||||
"reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
|
||||
"reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
|
||||
"reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e",
|
||||
"reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.0-dev"
|
||||
"dev-main": "8.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6829,7 +6989,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
|
||||
"security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6849,32 +7009,32 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-10T07:50:56+00:00"
|
||||
"time": "2026-02-06T04:51:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/type",
|
||||
"version": "4.0.0",
|
||||
"version": "7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/type.git",
|
||||
"reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
|
||||
"reference": "42412224607bd3931241bbd17f38e0f972f5a916"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
|
||||
"reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/42412224607bd3931241bbd17f38e0f972f5a916",
|
||||
"reference": "42412224607bd3931241bbd17f38e0f972f5a916",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0"
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "4.0-dev"
|
||||
"dev-main": "7.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6897,37 +7057,50 @@
|
|||
"homepage": "https://github.com/sebastianbergmann/type",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/type/issues",
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
|
||||
"security": "https://github.com/sebastianbergmann/type/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/7.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/type",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-03T07:10:45+00:00"
|
||||
"time": "2026-02-06T04:52:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/version",
|
||||
"version": "4.0.1",
|
||||
"version": "7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/version.git",
|
||||
"reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
|
||||
"reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
|
||||
"reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b",
|
||||
"reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "4.0-dev"
|
||||
"dev-main": "7.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -6950,15 +7123,28 @@
|
|||
"homepage": "https://github.com/sebastianbergmann/version",
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/version/issues",
|
||||
"source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
|
||||
"security": "https://github.com/sebastianbergmann/version/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/version/tree/7.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/version",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-07T11:34:05+00:00"
|
||||
"time": "2026-02-06T04:52:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastianfeldmann/camino",
|
||||
|
|
@ -7137,48 +7323,92 @@
|
|||
"time": "2026-01-26T20:59:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.4",
|
||||
"name": "staabm/side-effects-detector",
|
||||
"version": "1.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894"
|
||||
"url": "https://github.com/staabm/side-effects-detector.git",
|
||||
"reference": "d8334211a140ce329c13726d4a715adbddd0a163"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
|
||||
"reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
|
||||
"url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
|
||||
"reference": "d8334211a140ce329c13726d4a715adbddd0a163",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/string": "^7.2|^8.0"
|
||||
"ext-tokenizer": "*",
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/dotenv": "<6.4",
|
||||
"symfony/event-dispatcher": "<6.4",
|
||||
"symfony/lock": "<6.4",
|
||||
"symfony/process": "<6.4"
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^1.12.6",
|
||||
"phpunit/phpunit": "^9.6.21",
|
||||
"symfony/var-dumper": "^5.4.43",
|
||||
"tomasvotruba/type-coverage": "1.0.0",
|
||||
"tomasvotruba/unused-public": "1.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A static analysis tool to detect side effects in PHP code",
|
||||
"keywords": [
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/staabm/side-effects-detector/issues",
|
||||
"source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/staabm",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-10-20T05:08:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
|
||||
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"symfony/polyfill-mbstring": "^1.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/string": "^7.4|^8.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
"symfony/config": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^7.4|^8.0",
|
||||
"symfony/event-dispatcher": "^7.4|^8.0",
|
||||
"symfony/http-foundation": "^7.4|^8.0",
|
||||
"symfony/http-kernel": "^7.4|^8.0",
|
||||
"symfony/lock": "^7.4|^8.0",
|
||||
"symfony/messenger": "^7.4|^8.0",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/stopwatch": "^7.4|^8.0",
|
||||
"symfony/var-dumper": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -7212,7 +7442,7 @@
|
|||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7232,28 +7462,28 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-13T11:36:38+00:00"
|
||||
"time": "2026-01-13T13:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v7.4.4",
|
||||
"version": "v8.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "dc2c0eba1af673e736bb851d747d266108aea746"
|
||||
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746",
|
||||
"reference": "dc2c0eba1af673e736bb851d747d266108aea746",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
|
||||
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"symfony/event-dispatcher-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/security-http": "<7.4",
|
||||
"symfony/service-contracts": "<2.5"
|
||||
},
|
||||
"provide": {
|
||||
|
|
@ -7262,14 +7492,14 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/error-handler": "^6.4|^7.0|^8.0",
|
||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/config": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^7.4|^8.0",
|
||||
"symfony/error-handler": "^7.4|^8.0",
|
||||
"symfony/expression-language": "^7.4|^8.0",
|
||||
"symfony/framework-bundle": "^7.4|^8.0",
|
||||
"symfony/http-foundation": "^7.4|^8.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0"
|
||||
"symfony/stopwatch": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -7297,7 +7527,7 @@
|
|||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7317,7 +7547,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T11:45:34+00:00"
|
||||
"time": "2026-01-05T11:45:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
|
|
@ -7397,23 +7627,23 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.4.5",
|
||||
"version": "v8.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
|
||||
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/filesystem": "^6.4|^7.0|^8.0"
|
||||
"symfony/filesystem": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -7441,7 +7671,7 @@
|
|||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.4.5"
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7461,24 +7691,24 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:07:59+00:00"
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.4.0",
|
||||
"version": "v8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "b38026df55197f9e39a44f3215788edf83187b80"
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80",
|
||||
"reference": "b38026df55197f9e39a44f3215788edf83187b80",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"type": "library",
|
||||
|
|
@ -7512,7 +7742,7 @@
|
|||
"options"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v7.4.0"
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7532,7 +7762,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-12T15:39:26+00:00"
|
||||
"time": "2025-11-12T15:55:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
|
|
@ -7863,20 +8093,20 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.4.5",
|
||||
"version": "v8.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "608476f4604102976d687c483ac63a79ba18cc97"
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
|
||||
"reference": "608476f4604102976d687c483ac63a79ba18cc97",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -7904,7 +8134,7 @@
|
|||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.5"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -7924,7 +8154,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:07:59+00:00"
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
|
@ -8015,20 +8245,20 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v7.4.0",
|
||||
"version": "v8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
"reference": "8a24af0a2e8a872fb745047180649b8418303084"
|
||||
"reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084",
|
||||
"reference": "8a24af0a2e8a872fb745047180649b8418303084",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942",
|
||||
"reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"type": "library",
|
||||
|
|
@ -8057,7 +8287,7 @@
|
|||
"description": "Provides a way to profile code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v7.4.0"
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8077,39 +8307,38 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-04T07:05:15+00:00"
|
||||
"time": "2025-08-04T07:36:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.4.4",
|
||||
"version": "v8.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f"
|
||||
"reference": "758b372d6882506821ed666032e43020c4f57194"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f",
|
||||
"reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
|
||||
"reference": "758b372d6882506821ed666032e43020c4f57194",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.33",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
"php": ">=8.4",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-intl-grapheme": "^1.33",
|
||||
"symfony/polyfill-intl-normalizer": "^1.0",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/translation-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/emoji": "^7.4|^8.0",
|
||||
"symfony/http-client": "^7.4|^8.0",
|
||||
"symfony/intl": "^7.4|^8.0",
|
||||
"symfony/translation-contracts": "^2.5|^3.0",
|
||||
"symfony/var-exporter": "^6.4|^7.0|^8.0"
|
||||
"symfony/var-exporter": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -8148,7 +8377,7 @@
|
|||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8168,7 +8397,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-12T10:54:30+00:00"
|
||||
"time": "2026-01-12T12:37:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symplify/coding-standard",
|
||||
|
|
@ -8291,23 +8520,23 @@
|
|||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
"version": "1.3.1",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/theseer/tokenizer.git",
|
||||
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
|
||||
"reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
|
||||
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
|
||||
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
|
||||
"reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"php": "^7.2 || ^8.0"
|
||||
"php": "^8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -8329,7 +8558,7 @@
|
|||
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
|
||||
"support": {
|
||||
"issues": "https://github.com/theseer/tokenizer/issues",
|
||||
"source": "https://github.com/theseer/tokenizer/tree/1.3.1"
|
||||
"source": "https://github.com/theseer/tokenizer/tree/2.0.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8337,12 +8566,13 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-17T20:03:58+00:00"
|
||||
"time": "2025-12-08T11:19:18+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"adaures/castopod-plugins-manager": 20,
|
||||
"codeigniter4/tasks": 20,
|
||||
"opawg/user-agents-v2-php": 20,
|
||||
"yassinedoghri/podcast-feed": 20
|
||||
|
|
@ -8350,7 +8580,7 @@
|
|||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.1"
|
||||
"php": "^8.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# ⚠️ NOT optimized for production
|
||||
# should be used only for continuous integration
|
||||
#---------------------------------------------------
|
||||
FROM php:8.1-fpm-alpine3.22
|
||||
FROM php:8.5-fpm-alpine3.23
|
||||
|
||||
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
|
||||
|
||||
|
|
@ -32,9 +32,8 @@ RUN \
|
|||
mysqli \
|
||||
&& apk del .php-ext-build-dep \
|
||||
# install pnpm
|
||||
&& wget -qO- https://get.pnpm.io/install.sh | ENV="~/.shrc" SHELL="$(which sh)" sh - \
|
||||
&& mv ~/.local/share/pnpm/pnpm /usr/bin/pnpm \
|
||||
&& rm -rf ~/.local \
|
||||
&& npm install --global corepack@latest \
|
||||
&& corepack enable pnpm \
|
||||
# set pnpm store directory
|
||||
&& pnpm config set store-dir .pnpm-store \
|
||||
# set composer cache directory
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue