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 |
1170 changed files with 35992 additions and 65291 deletions
|
|
@ -3,17 +3,15 @@
|
||||||
"projectOwner": "adaures",
|
"projectOwner": "adaures",
|
||||||
"repoType": "gitlab",
|
"repoType": "gitlab",
|
||||||
"repoHost": "https://code.castopod.org",
|
"repoHost": "https://code.castopod.org",
|
||||||
"files": ["README.md", "docs/src/content/docs/en/index.mdx"],
|
"files": ["README.md"],
|
||||||
"imageSize": 100,
|
"imageSize": 100,
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"contributorsPerLine": 7,
|
|
||||||
"wrapperTemplate": "\n<table class=\"all-contributors-table\">\n <tbody><%= bodyContent %> </tbody>\n<%= tableFooterContent %></table>\n\n",
|
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"login": "yassinedoghri",
|
"login": "yassinedoghri",
|
||||||
"name": "Yassine Doghri",
|
"name": "Yassine Doghri",
|
||||||
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/3/avatar.png",
|
"avatar_url": "https://avatars.githubusercontent.com/u/11021441?v=4",
|
||||||
"profile": "https://github.com/yassinedoghri",
|
"profile": "https://yassinedoghri.com",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"code",
|
"code",
|
||||||
"bug",
|
"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",
|
"login": "basen1982",
|
||||||
"name": "Andreas Olsson",
|
"name": "Andreas Olsson",
|
||||||
|
|
@ -545,7 +531,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "ahmed.sabouni11",
|
"login": "ahmedsabouni",
|
||||||
"name": "Ahmed Sabouni",
|
"name": "Ahmed Sabouni",
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
|
||||||
"profile": "https://github.com/ahmedsabouni",
|
"profile": "https://github.com/ahmedsabouni",
|
||||||
|
|
@ -564,11 +550,25 @@
|
||||||
"contributions": ["code"]
|
"contributions": ["code"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "NeoluxConsulting",
|
"login": "Dwev",
|
||||||
"name": "Guy Martin",
|
"name": "Guy Martin",
|
||||||
"avatar_url": "https://secure.gravatar.com/avatar/6e745565356330c1e29a85d52bffdaa1?s=80&d=identicon",
|
"avatar_url": "https://avatars.githubusercontent.com/u/46626050?v=4",
|
||||||
"profile": "https://code.castopod.org/NeoluxConsulting",
|
"profile": "https://github.com/Dwev",
|
||||||
"contributions": ["bug", "code"]
|
"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"
|
"commitConvention": "none"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
# ⚠️ NOT optimized for production
|
# ⚠️ NOT optimized for production
|
||||||
# should be used only for development purposes
|
# should be used only for development purposes
|
||||||
#---------------------------------------------------
|
#---------------------------------------------------
|
||||||
FROM php:8.2-fpm
|
FROM php:8.5-fpm
|
||||||
|
|
||||||
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
|
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
# Install server requirements
|
# Install server requirements
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install --yes --no-install-recommends nodejs \
|
&& apt-get install --yes --no-install-recommends nodejs \
|
||||||
# gnupg to sign commits with gpg
|
# gnupg to sign commits with gpg
|
||||||
|
|
@ -47,11 +47,4 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& docker-php-ext-enable redis \
|
&& docker-php-ext-enable redis \
|
||||||
# mysqli for database access
|
# mysqli for database access
|
||||||
&& docker-php-ext-install mysqli \
|
&& docker-php-ext-install mysqli \
|
||||||
&& docker-php-ext-enable 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
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
"postCreateCommand": "composer install && pnpm install && pnpm run build:static && php spark migrate --all && php spark db:seed DevSeeder",
|
"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",
|
"postAttachCommand": "crontab .devcontainer/crontab && service cron reload",
|
||||||
"shutdownAction": "stopCompose",
|
"shutdownAction": "stopCompose",
|
||||||
"features": {
|
"features": {
|
||||||
|
|
@ -30,7 +30,17 @@
|
||||||
"spark": "php",
|
"spark": "php",
|
||||||
"env": "dotenv",
|
"env": "dotenv",
|
||||||
".rsync-filter": "diff"
|
".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": [
|
"extensions": [
|
||||||
"astro-build.astro-vscode",
|
"astro-build.astro-vscode",
|
||||||
|
|
@ -52,7 +62,8 @@
|
||||||
"stylelint.vscode-stylelint",
|
"stylelint.vscode-stylelint",
|
||||||
"unifiedjs.vscode-mdx",
|
"unifiedjs.vscode-mdx",
|
||||||
"wayou.vscode-todo-highlight",
|
"wayou.vscode-todo-highlight",
|
||||||
"yzhang.markdown-all-in-one"
|
"yzhang.markdown-all-in-one",
|
||||||
|
"42Crunch.vscode-openapi"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/workspaces:cached
|
- ../..:/workspaces:cached
|
||||||
|
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||||
environment:
|
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
|
CI_ENVIRONMENT: development
|
||||||
vite_environment: development
|
vite_environment: development
|
||||||
app_forceGlobalSecureRequests: 0 #false
|
app_forceGlobalSecureRequests: 0 #false
|
||||||
app_baseURL: http://localhost:8080/
|
app_baseURL: http://localhost:${APP_PORT:-8080}/
|
||||||
media_baseURL: http://localhost:8080/
|
media_baseURL: http://localhost:${APP_PORT:-8080}/
|
||||||
admin_gateway: cp-admin
|
admin_gateway: cp-admin
|
||||||
auth_gateway: cp-auth
|
auth_gateway: cp-auth
|
||||||
analytics_salt: dev_analytics_salt
|
analytics_salt: dev_analytics_salt
|
||||||
|
|
@ -29,16 +28,10 @@ services:
|
||||||
email_SMTPHost: mailpit
|
email_SMTPHost: mailpit
|
||||||
email_SMTPUser: castopod
|
email_SMTPUser: castopod
|
||||||
email_SMTPPass: castopod
|
email_SMTPPass: castopod
|
||||||
email_SMTPPort: 1025
|
email_SMTPPort: ${MAILPIT_SMTP_PORT:-1025}
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
|
||||||
- mariadb
|
- mariadb
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:alpine
|
|
||||||
volumes:
|
|
||||||
- redis:/data
|
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:10.2
|
image: mariadb:10.2
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -69,8 +62,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- mailpit:/data
|
- mailpit:/data
|
||||||
ports:
|
ports:
|
||||||
- 8025:8025
|
- ${MAILPIT_WEBUI_PORT:-8025}:8025
|
||||||
- 1025:1025
|
- ${MAILPIT_SMTP_PORT:-1025}:1025
|
||||||
environment:
|
environment:
|
||||||
MP_MAX_MESSAGES: 5000
|
MP_MAX_MESSAGES: 5000
|
||||||
MP_DATA_FILE: /data/mailpit.db
|
MP_DATA_FILE: /data/mailpit.db
|
||||||
|
|
@ -78,7 +71,6 @@ services:
|
||||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis:
|
|
||||||
mariadb:
|
mariadb:
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
mailpit:
|
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
|
||||||
68
.dockerignore
Normal file
68
.dockerignore
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
.env
|
||||||
|
|
||||||
|
.git/
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
build/
|
||||||
|
docs/
|
||||||
|
scripts/
|
||||||
|
tests/
|
||||||
|
|
||||||
|
#-------------------------
|
||||||
|
# Temporary Files
|
||||||
|
#-------------------------
|
||||||
|
writable/cache/*
|
||||||
|
!writable/cache/index.html
|
||||||
|
|
||||||
|
writable/logs/*
|
||||||
|
!writable/logs/index.html
|
||||||
|
|
||||||
|
writable/session/*
|
||||||
|
!writable/session/index.html
|
||||||
|
|
||||||
|
writable/temp/*
|
||||||
|
!writable/temp/index.html
|
||||||
|
|
||||||
|
writable/uploads/*
|
||||||
|
!writable/uploads/index.html
|
||||||
|
|
||||||
|
writable/debugbar/*
|
||||||
|
!writable/debugbar/index.html
|
||||||
|
|
||||||
|
# public folder
|
||||||
|
public/*
|
||||||
|
!public/media
|
||||||
|
!public/.htaccess
|
||||||
|
!public/favicon.ico
|
||||||
|
!public/icon*
|
||||||
|
!public/castopod-banner*
|
||||||
|
!public/castopod-avatar*
|
||||||
|
!public/index.php
|
||||||
|
!public/robots.txt
|
||||||
|
!public/.well-known
|
||||||
|
!public/.well-known/GDPR.yml
|
||||||
|
|
||||||
|
public/assets/*
|
||||||
|
!public/assets/index.html
|
||||||
|
|
||||||
|
# public media folder
|
||||||
|
!public/media/podcasts
|
||||||
|
!public/media/persons
|
||||||
|
!public/media/site
|
||||||
|
|
||||||
|
public/media/podcasts/*
|
||||||
|
!public/media/podcasts/index.html
|
||||||
|
|
||||||
|
public/media/persons/*
|
||||||
|
!public/media/persons/index.html
|
||||||
|
|
||||||
|
public/media/site/*
|
||||||
|
!public/media/site/index.html
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
modules/Admin/Language/*/PersonsTaxonomy.php
|
||||||
|
|
||||||
|
# Castopod bundle & packages
|
||||||
|
castopod/
|
||||||
|
castopod-*.zip
|
||||||
|
castopod-*.tar.gz
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2020": true
|
|
||||||
},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 11,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
36
.gitignore
vendored
36
.gitignore
vendored
|
|
@ -67,7 +67,7 @@ writable/uploads/*
|
||||||
!writable/uploads/index.html
|
!writable/uploads/index.html
|
||||||
|
|
||||||
writable/debugbar/*
|
writable/debugbar/*
|
||||||
!writable/debugbar/.gitkeep
|
!writable/debugbar/index.html
|
||||||
|
|
||||||
php_errors.log
|
php_errors.log
|
||||||
|
|
||||||
|
|
@ -107,15 +107,15 @@ _modules/*
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Netbeans
|
# NetBeans
|
||||||
nbproject/
|
/nbproject/
|
||||||
build/
|
/build/
|
||||||
nbbuild/
|
/nbbuild/
|
||||||
dist/
|
/dist/
|
||||||
nbdist/
|
/nbdist/
|
||||||
nbactions.xml
|
/nbactions.xml
|
||||||
nb-configuration.xml
|
/nb-configuration.xml
|
||||||
.nb-gradle/
|
/.nb-gradle/
|
||||||
|
|
||||||
# Sublime Text
|
# Sublime Text
|
||||||
*.tmlanguage.cache
|
*.tmlanguage.cache
|
||||||
|
|
@ -128,6 +128,7 @@ nb-configuration.xml
|
||||||
|
|
||||||
# Visual Studio Code
|
# Visual Studio Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.history/
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
/results/
|
/results/
|
||||||
|
|
@ -174,16 +175,13 @@ public/media/site/*
|
||||||
# Generated files
|
# Generated files
|
||||||
modules/Admin/Language/*/PersonsTaxonomy.php
|
modules/Admin/Language/*/PersonsTaxonomy.php
|
||||||
|
|
||||||
#-------------------------
|
|
||||||
# Docker volumes
|
|
||||||
#-------------------------
|
|
||||||
|
|
||||||
mariadb
|
|
||||||
phpmyadmin
|
|
||||||
sessions
|
|
||||||
data
|
|
||||||
|
|
||||||
# Castopod bundle & packages
|
# Castopod bundle & packages
|
||||||
castopod/
|
castopod/
|
||||||
castopod-*.zip
|
castopod-*.zip
|
||||||
castopod-*.tar.gz
|
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
|
image: code.castopod.org:5050/adaures/castopod:ci-php8.5
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- prepare
|
- prepare
|
||||||
|
|
@ -23,6 +23,10 @@ php-dependencies:
|
||||||
expire_in: 30 mins
|
expire_in: 30 mins
|
||||||
paths:
|
paths:
|
||||||
- vendor/
|
- vendor/
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
js-dependencies:
|
js-dependencies:
|
||||||
stage: prepare
|
stage: prepare
|
||||||
|
|
@ -39,6 +43,10 @@ js-dependencies:
|
||||||
expire_in: 30 mins
|
expire_in: 30 mins
|
||||||
paths:
|
paths:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
lint-commit-msg:
|
lint-commit-msg:
|
||||||
stage: quality
|
stage: quality
|
||||||
|
|
@ -48,11 +56,10 @@ lint-commit-msg:
|
||||||
- ./scripts/lint-commit-msg.sh
|
- ./scripts/lint-commit-msg.sh
|
||||||
dependencies:
|
dependencies:
|
||||||
- js-dependencies
|
- js-dependencies
|
||||||
only:
|
rules:
|
||||||
- develop
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
- main
|
when: never
|
||||||
- beta
|
- if: $CI_COMMIT_BRANCH =~ /^(develop|main|alpha|beta|next)$/
|
||||||
- alpha
|
|
||||||
|
|
||||||
lint-php:
|
lint-php:
|
||||||
stage: quality
|
stage: quality
|
||||||
|
|
@ -65,34 +72,46 @@ lint-php:
|
||||||
- vendor/bin/rector process --dry-run --ansi
|
- vendor/bin/rector process --dry-run --ansi
|
||||||
dependencies:
|
dependencies:
|
||||||
- php-dependencies
|
- php-dependencies
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
lint-js:
|
lint-js:
|
||||||
stage: quality
|
stage: quality
|
||||||
script:
|
script:
|
||||||
- pnpm run prettier
|
- pnpm run format
|
||||||
- pnpm run typecheck
|
- pnpm run typecheck
|
||||||
- pnpm run lint
|
- pnpm run lint
|
||||||
- pnpm run lint:css
|
- pnpm run lint:css
|
||||||
dependencies:
|
dependencies:
|
||||||
- js-dependencies
|
- js-dependencies
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
stage: quality
|
stage: quality
|
||||||
services:
|
services:
|
||||||
- mariadb:10.2
|
- mariadb:10.11
|
||||||
variables:
|
variables:
|
||||||
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
|
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
|
||||||
MYSQL_DATABASE: "test"
|
MYSQL_DATABASE: "test"
|
||||||
MYSQL_USER: "castopod"
|
MYSQL_USER: "castopod"
|
||||||
MYSQL_PASSWORD: "castopod"
|
MYSQL_PASSWORD: "castopod"
|
||||||
script:
|
script:
|
||||||
- echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE"
|
- echo "SHOW DATABASES;" | mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip_ssl
|
||||||
|
|
||||||
# run phpunit without code coverage
|
# run phpunit without code coverage
|
||||||
# TODO: add code coverage
|
# TODO: add code coverage
|
||||||
- vendor/bin/phpunit --no-coverage
|
- vendor/bin/phpunit --no-coverage
|
||||||
dependencies:
|
dependencies:
|
||||||
- php-dependencies
|
- php-dependencies
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
bundle:
|
bundle:
|
||||||
stage: bundle
|
stage: bundle
|
||||||
|
|
@ -113,13 +132,12 @@ bundle:
|
||||||
name: "castopod-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
|
name: "castopod-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
|
||||||
paths:
|
paths:
|
||||||
- castopod
|
- castopod
|
||||||
only:
|
rules:
|
||||||
variables:
|
- if: $CI_PROJECT_NAMESPACE != "adaures"
|
||||||
- $CI_PROJECT_NAMESPACE == "adaures"
|
when: never
|
||||||
except:
|
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/ || $CI_COMMIT_TAG
|
||||||
- main
|
when: never
|
||||||
- beta
|
- when: on_success
|
||||||
- alpha
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
stage: release
|
stage: release
|
||||||
|
|
@ -143,38 +161,38 @@ release:
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- castopod
|
- castopod
|
||||||
- CP_VERSION.env
|
rules:
|
||||||
only:
|
- if: $CI_PROJECT_NAMESPACE != "adaures"
|
||||||
- main
|
when: never
|
||||||
- beta
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
- alpha
|
when: never
|
||||||
|
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/
|
||||||
|
|
||||||
website:
|
website:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
trigger: adaures/castopod.org
|
trigger: adaures/castopod.org
|
||||||
only:
|
rules:
|
||||||
- main
|
- if: $CI_PROJECT_NAMESPACE != "adaures"
|
||||||
- beta
|
when: never
|
||||||
- alpha
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
trigger:
|
trigger:
|
||||||
include: docs/.gitlab-ci.yml
|
include: docs/.gitlab-ci.yml
|
||||||
strategy: depend
|
strategy: depend
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
|
||||||
|
when: never
|
||||||
|
- when: on_success
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
stage: build
|
stage: build
|
||||||
trigger:
|
trigger:
|
||||||
include: docker/production/.gitlab-ci.yml
|
include: docker/production/.gitlab-ci.yml
|
||||||
strategy: depend
|
strategy: depend
|
||||||
variables:
|
rules:
|
||||||
PARENT_PIPELINE_ID: $CI_PIPELINE_ID
|
- if: $CI_PROJECT_NAMESPACE != "adaures"
|
||||||
only:
|
when: never
|
||||||
refs:
|
- if: $CI_COMMIT_BRANCH == "develop"
|
||||||
- develop
|
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG
|
||||||
- main
|
|
||||||
- beta
|
|
||||||
- alpha
|
|
||||||
variables:
|
|
||||||
- $CI_PROJECT_NAMESPACE == "adaures"
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
1. [First step]
|
1. [First step]
|
||||||
2. [Second step]
|
2. [Second step]
|
||||||
3. [and so on...]
|
3. [and so on…]
|
||||||
|
|
||||||
### Expected behavior
|
### Expected behavior
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ logs, and code as it's very hard to read otherwise.
|
||||||
- OS: [e.g. Ubuntu server]
|
- OS: [e.g. Ubuntu server]
|
||||||
- Browser: [e.g. chrome, safari]
|
- Browser: [e.g. chrome, safari]
|
||||||
- Web server: [eg. Apache]
|
- Web server: [eg. Apache]
|
||||||
- [any other relevant context...]
|
- [any other relevant context…]
|
||||||
|
|
||||||
### Possible fixes
|
### Possible fixes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
### Is your feature request related to a problem? Please describe
|
### Is your feature request related to a problem? Please describe
|
||||||
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always
|
A clear and concise description of what the problem is. Ex. I'm always
|
||||||
frustrated when [...]
|
frustrated when […]
|
||||||
|
|
||||||
### Describe the solution you'd like
|
### Describe the solution you'd like
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "*.md",
|
"files": ["*.md", "*.mdx"],
|
||||||
"options": {
|
"options": {
|
||||||
"proseWrap": "always"
|
"proseWrap": "always"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,74 @@
|
||||||
{
|
{
|
||||||
"name": "beta",
|
"name": "beta",
|
||||||
"prerelease": true
|
"prerelease": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "next",
|
||||||
|
"prerelease": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@semantic-release/commit-analyzer",
|
[
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits",
|
||||||
|
"releaseRules": [
|
||||||
|
{
|
||||||
|
"type": "docs",
|
||||||
|
"scope": "README",
|
||||||
|
"release": "patch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "refactor",
|
||||||
|
"scope": "core-*",
|
||||||
|
"release": "minor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "refactor",
|
||||||
|
"release": "patch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parserOpts": {
|
||||||
|
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits",
|
||||||
|
"parserOpts": {
|
||||||
|
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
|
||||||
|
},
|
||||||
|
"presetConfig": {
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "feat",
|
||||||
|
"section": "Features"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fix",
|
||||||
|
"section": "Bug Fixes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"section": "Internal",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "refactor",
|
||||||
|
"section": "Internal",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "perf",
|
||||||
|
"section": "Internal",
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
[
|
[
|
||||||
"@semantic-release/exec",
|
"@semantic-release/exec",
|
||||||
|
|
@ -30,7 +93,8 @@
|
||||||
"package.json",
|
"package.json",
|
||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"CHANGELOG.md"
|
"CHANGELOG.md"
|
||||||
]
|
],
|
||||||
|
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# rsync filter rules to copy required files for Castopod's bundle
|
# rsync filter rules to copy required files for Castopod's bundle
|
||||||
|
|
||||||
+ app/Resources/icons/***
|
+ resources/icons/***
|
||||||
- app/Resources/**
|
+ resources/
|
||||||
+ app/***
|
+ app/***
|
||||||
+ modules/***
|
+ modules/***
|
||||||
|
+ plugins/***
|
||||||
+ public/***
|
+ public/***
|
||||||
+ themes/***
|
+ themes/***
|
||||||
+ vendor/***
|
+ vendor/***
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,17 @@
|
||||||
"responsive",
|
"responsive",
|
||||||
"variants",
|
"variants",
|
||||||
"screen",
|
"screen",
|
||||||
"layer"
|
"layer",
|
||||||
|
"config"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"at-rule-no-deprecated": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreAtRules": ["apply"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"function-no-unknown": [
|
"function-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
182
CHANGELOG.md
182
CHANGELOG.md
|
|
@ -1,3 +1,185 @@
|
||||||
|
## [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
|
||||||
|
([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
|
||||||
|
([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
|
||||||
|
([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
|
||||||
|
([63c763f](https://code.castopod.org/adaures/castopod/commit/63c763f941195b3758c4b91acd8c350a5e7bb9c2)),
|
||||||
|
closes [#510](https://code.castopod.org/adaures/castopod/issues/510)
|
||||||
|
- 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
|
||||||
|
([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))
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
- **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))
|
||||||
|
|
||||||
|
# [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
|
||||||
|
([0ba0a25](https://code.castopod.org/adaures/castopod/commit/0ba0a25b11bd67aeeb47a8179b72152dfd4a36da))
|
||||||
|
- broken icon call in frontend default pages template
|
||||||
|
([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
|
||||||
|
([5d35524](https://code.castopod.org/adaures/castopod/commit/5d355248753be24e3cf324144ff076f2fc23be88)),
|
||||||
|
closes [#500](https://code.castopod.org/adaures/castopod/issues/500)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
- add missing php-icons config file to bundle
|
||||||
|
([56612f0](https://code.castopod.org/adaures/castopod/commit/56612f0c762aa2d98e3c8c77fba88ffdf6f46a44))
|
||||||
|
- **docs:** add base to og image using env variable
|
||||||
|
([fe67659](https://code.castopod.org/adaures/castopod/commit/fe676590f23a33bdbe8905d234760923c029e350))
|
||||||
|
- **import:** rewrite download_file helper to output curl response directly to
|
||||||
|
file
|
||||||
|
([eb7ad2f](https://code.castopod.org/adaures/castopod/commit/eb7ad2f7e1c0137f222f47e47062887de42c4824))
|
||||||
|
- include app/Resources/icons folder to bundle
|
||||||
|
([3fd5efc](https://code.castopod.org/adaures/castopod/commit/3fd5efc7956977acc19e53182f25b12813964a7d))
|
||||||
|
- **platforms:** add platforms service + reduce memory consumption when
|
||||||
|
rendering platform cards
|
||||||
|
([fe73e9f](https://code.castopod.org/adaures/castopod/commit/fe73e9fae9ea5d5ce946680aec194308bb2e620c))
|
||||||
|
- set owner email visibility when editing podcast
|
||||||
|
([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)
|
# [1.11.0](https://code.castopod.org/adaures/castopod/compare/v1.10.5...v1.11.0) (4/17/2024)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,162 @@
|
||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant 3.0 Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
We pledge to make our community welcoming, safe, and equitable for all.
|
||||||
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 act and interact in ways that contribute to an open, welcoming,
|
We are committed to fostering an environment that respects and promotes the
|
||||||
diverse, inclusive, and healthy community.
|
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
|
While acknowledging differences in social norms, we all strive to meet our
|
||||||
community include:
|
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
|
With these considerations in mind, we agree to behave mindfully toward each
|
||||||
- Being respectful of differing opinions, viewpoints, and experiences
|
other and act in ways that center our shared values, including:
|
||||||
- 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
|
|
||||||
|
|
||||||
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
|
## Restricted Behaviors
|
||||||
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
|
|
||||||
|
|
||||||
## 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
|
1. **Harassment.** Violating explicitly expressed boundaries or engaging in
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
unnecessary personal attention after any clear request to stop.
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
2. **Character attacks.** Making insulting, demeaning, or pejorative comments
|
||||||
or harmful.
|
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
|
### Other Restrictions
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
1. **Misleading identity.** Impersonating someone else for any reason, or
|
||||||
decisions when appropriate.
|
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
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
an individual is officially representing the community in public spaces.
|
an individual is officially representing the community in public or other
|
||||||
Examples of representing our community include using an official e-mail address,
|
spaces. Examples of representing our community include using an official email
|
||||||
posting via an official social media account, or acting as an appointed
|
address, posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event.
|
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
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the Contributor Covenant, version 3.0,
|
||||||
version 2.0, available at
|
permanently available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by
|
Contributor Covenant is stewarded by the Organization for Ethical Source and
|
||||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
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 Contributor Covenant, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
Translations are provided at
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
[https://www.contributor-covenant.org/translations](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
|
Castopod is a web app based on the `php` framework
|
||||||
[CodeIgniter 4](https://codeigniter.com).
|
[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
|
`docker-compose.yml` and `Dockerfile` are included in the project's root folder
|
||||||
to help you kickstart your contribution.
|
to help you kickstart your contribution.
|
||||||
|
|
||||||
|
|
@ -16,9 +16,9 @@ to help you kickstart your contribution.
|
||||||
|
|
||||||
### 1. Pre-requisites
|
### 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
|
```bash
|
||||||
git clone https://code.castopod.org/adaures/castopod.git
|
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)
|
> [CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html)
|
||||||
> for more info.
|
> 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`
|
`Settings` > `Resources` > `File Sharing`
|
||||||
|
|
||||||
### 2. (recommended) Develop inside the app container with VSCode
|
### 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
|
> The VSCode window will reload inside the dev container. Expect several
|
||||||
> minutes during first load as it is building all necessary services.
|
> 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
|
During development, you will have to start [Vite](https://vitejs.dev)'s dev
|
||||||
server for compiling the typescript code and styles:
|
server for compiling the typescript code and styles:
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ required services will be loaded automagically! 🪄
|
||||||
pnpm run dev
|
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:
|
using the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -146,12 +146,10 @@ To see your changes, go to:
|
||||||
|
|
||||||
- `http://localhost:8080/` for the Castopod website
|
- `http://localhost:8080/` for the Castopod website
|
||||||
- `http://localhost:8080/cp-admin` for the Castopod admin:
|
- `http://localhost:8080/cp-admin` for the Castopod admin:
|
||||||
|
|
||||||
- email: **admin@castopod.local**
|
- email: **admin@castopod.local**
|
||||||
- password: **castopod**
|
- password: **castopod**
|
||||||
|
|
||||||
- `http://localhost:8888/` for the phpmyadmin interface:
|
- `http://localhost:8888/` for the phpmyadmin interface:
|
||||||
|
|
||||||
- username: **castopod**
|
- username: **castopod**
|
||||||
- password: **castopod**
|
- password: **castopod**
|
||||||
|
|
||||||
|
|
@ -159,9 +157,9 @@ To see your changes, go to:
|
||||||
|
|
||||||
You do not wish to use the VSCode devcontainer? No problem!
|
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
|
```bash
|
||||||
# starts all services declared in docker-compose.yml file
|
# starts all services declared in docker-compose.yml file
|
||||||
|
|
@ -225,14 +223,14 @@ You do not wish to use the VSCode devcontainer? No problem!
|
||||||
> For more info, check out the
|
> For more info, check out the
|
||||||
> [Composer documentation](https://getcomposer.org/doc/).
|
> [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
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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
|
> check the `package.json` and `pnpm-lock.yaml` files to download the
|
||||||
> packages with the right versions. The dependencies will live under the
|
> packages with the right versions. The dependencies will live under the
|
||||||
> `node_module` folder. For more info, check out the
|
> `node_module` folder. For more info, check out the
|
||||||
|
|
@ -251,7 +249,7 @@ You do not wish to use the VSCode devcontainer? No problem!
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The static assets generated live under the `public/assets` folder, it
|
> 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
|
### Initialize and populate database
|
||||||
|
|
||||||
|
|
@ -293,8 +291,7 @@ You do not wish to use the VSCode devcontainer? No problem!
|
||||||
php spark db:seed DevSuperadminSeeder
|
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:
|
- Populate with fake podcast analytics:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -315,13 +312,13 @@ You do not wish to use the VSCode devcontainer? No problem!
|
||||||
docker-compose logs --tail 50 --follow --timestamps app
|
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
|
```bash
|
||||||
docker exec -it castopod_redis redis-cli
|
docker exec -it castopod_redis redis-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
- Monitor the redis container:
|
- Monitor the Redis container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose logs --tail 50 --follow --timestamps redis
|
docker-compose logs --tail 50 --follow --timestamps redis
|
||||||
|
|
@ -357,10 +354,37 @@ docker-compose down
|
||||||
docker-compose build app
|
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
|
[docker-compose](https://docs.docker.com/compose/reference/) documentations for
|
||||||
more insights.
|
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
|
## Known issues
|
||||||
|
|
||||||
### Allocation failed - JavaScript heap out of memory
|
### Allocation failed - JavaScript heap out of memory
|
||||||
|
|
@ -399,7 +423,7 @@ You may use Linux user namespaces to fix this on your machine:
|
||||||
username:100000:65536
|
username:100000:65536
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Restart docker:
|
3. Restart Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl restart docker
|
sudo systemctl restart docker
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ experience the problem? What would you expect to be the outcome? All these
|
||||||
details will help people to fix any potential bugs.
|
details will help people to fix any potential bugs.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates)
|
> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates) have
|
||||||
> have been created for this project. You may use them to help you follow those
|
> been created for this project. You may use them to help you follow those
|
||||||
> guidelines.
|
> guidelines.
|
||||||
|
|
||||||
## Feature requests
|
## Feature requests
|
||||||
|
|
@ -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
|
Adhering to the following process is the best way to get your work included in
|
||||||
the project:
|
the project:
|
||||||
|
|
||||||
1. [Fork](https://docs.gitlab.com/ee/gitlab-basics/fork-project.html) the
|
1. [Fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html)
|
||||||
project, clone your fork, and configure the remotes:
|
the project, clone your fork, and configure the remotes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone your fork of the repo into the current directory
|
# 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))
|
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
|
||||||
- [RemixIcon](https://remixicon.com/)
|
- [RemixIcon](https://remixicon.com/)
|
||||||
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
|
([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))
|
([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)
|
- [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents)
|
||||||
([by Open Podcast Analytics Working Group](https://github.com/opawg))
|
([by Open Podcast Analytics Working Group](https://github.com/opawg))
|
||||||
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))
|
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))
|
||||||
|
|
|
||||||
36
README.md
36
README.md
|
|
@ -1,7 +1,7 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>
|
<h1>
|
||||||
<a href="https://castopod.org/">
|
<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>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,18 +46,17 @@ Thanks goes to these wonderful people
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
<!-- markdownlint-disable -->
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
<table class="all-contributors-table">
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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://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://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://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://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://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>
|
<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>
|
<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://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>
|
<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/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/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>
|
<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>
|
<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/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>
|
<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/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/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>
|
<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>
|
<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/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>
|
<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://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://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>
|
<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>
|
<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://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>
|
<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/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/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>
|
<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>
|
<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/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>
|
<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://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://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>
|
<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>
|
<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/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/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/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/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://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>
|
<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://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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
|
@ -143,7 +141,7 @@ Alternatively, you can follow us on social media platforms to get news about
|
||||||
Castopod:
|
Castopod:
|
||||||
|
|
||||||
- [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance)
|
- [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance)
|
||||||
- [Twitter](https://twitter.com/castopod)
|
- [Bluesky](https://bsky.app/profile/castopod.org)
|
||||||
- [LinkedIn](https://linkedin.com/company/castopod)
|
- [LinkedIn](https://linkedin.com/company/castopod)
|
||||||
- [Facebook](https://www.facebook.com/castopod)
|
- [Facebook](https://www.facebook.com/castopod)
|
||||||
|
|
||||||
|
|
@ -157,10 +155,10 @@ backers. If you'd like to help, please consider
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<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>
|
||||||
<td align="center">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
50
app/Commands/EpisodesComputeDownloads.php
Normal file
50
app/Commands/EpisodesComputeDownloads.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Commands;
|
||||||
|
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
use CodeIgniter\CLI\BaseCommand;
|
||||||
|
|
||||||
|
class EpisodesComputeDownloads extends BaseCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Command's Group
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $group = 'Episodes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name = 'episodes:compute-downloads';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Description
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = "Calculates all episodes downloads and stores results in episodes' downloads_count field.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually execute a command.
|
||||||
|
*/
|
||||||
|
public function run(array $params): void
|
||||||
|
{
|
||||||
|
$episodesModel = new EpisodeModel();
|
||||||
|
$query = $episodesModel->builder()
|
||||||
|
->select('episodes.id as id, IFNULL(SUM(ape.hits),0) as downloads_count')
|
||||||
|
->join('analytics_podcasts_by_episode ape', 'episodes.id=ape.episode_id', 'left')
|
||||||
|
->groupBy('episodes.id');
|
||||||
|
|
||||||
|
$episodeModel2 = new EpisodeModel();
|
||||||
|
$episodeModel2->builder()
|
||||||
|
->setQueryAsData($query)
|
||||||
|
->onConstraint('id')
|
||||||
|
->updateBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ if (! function_exists('view')) {
|
||||||
$renderer = single_service('renderer', $path);
|
$renderer = single_service('renderer', $path);
|
||||||
|
|
||||||
$saveData = config('View')
|
$saveData = config('View')
|
||||||
->saveData;
|
->saveData;
|
||||||
|
|
||||||
if (array_key_exists('saveData', $options)) {
|
if (array_key_exists('saveData', $options)) {
|
||||||
$saveData = (bool) $options['saveData'];
|
$saveData = (bool) $options['saveData'];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class App extends BaseConfig
|
class App extends BaseConfig
|
||||||
{
|
{
|
||||||
|
|
@ -192,9 +193,9 @@ class App extends BaseConfig
|
||||||
* '192.168.5.0/24' => 'X-Real-IP',
|
* '192.168.5.0/24' => 'X-Real-IP',
|
||||||
* ]
|
* ]
|
||||||
*
|
*
|
||||||
* @var array<string, string>
|
* @var array<string, string>|string
|
||||||
*/
|
*/
|
||||||
public array $proxyIPs = [];
|
public $proxyIPs = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|
@ -249,4 +250,37 @@ class App extends BaseConfig
|
||||||
public ?int $bandwidthLimit = null;
|
public ?int $bandwidthLimit = null;
|
||||||
|
|
||||||
public ?string $legalNoticeURL = null;
|
public ?string $legalNoticeURL = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthToken Config Constructor
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
if (is_string($this->proxyIPs)) {
|
||||||
|
$array = json_decode($this->proxyIPs, true);
|
||||||
|
if (is_array($array)) {
|
||||||
|
$this->proxyIPs = $array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override parent initEnvValue() to allow for direct setting to array properties values from ENV
|
||||||
|
*
|
||||||
|
* In order to set array properties via ENV vars we need to set the property to a string value first.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
if ($name === 'proxyIPs' && $this->getEnvValue($name, $prefix, $shortPrefix) !== null) {
|
||||||
|
$property = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::initEnvValue($property, $name, $prefix, $shortPrefix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ use CodeIgniter\Config\AutoloadConfig;
|
||||||
*
|
*
|
||||||
* NOTE: If you use an identical key in $psr4 or $classmap, then
|
* NOTE: If you use an identical key in $psr4 or $classmap, then
|
||||||
* the values in this file will overwrite the framework's values.
|
* the values in this file will overwrite the framework's values.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Autoload extends AutoloadConfig
|
class Autoload extends AutoloadConfig
|
||||||
{
|
{
|
||||||
|
|
@ -50,6 +48,7 @@ class Autoload extends AutoloadConfig
|
||||||
'Modules\Media' => ROOTPATH . 'modules/Media/',
|
'Modules\Media' => ROOTPATH . 'modules/Media/',
|
||||||
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
|
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
|
||||||
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
|
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
|
||||||
|
'Modules\Plugins' => ROOTPATH . 'modules/Plugins/',
|
||||||
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
|
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
|
||||||
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
|
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
|
||||||
'Modules\Update' => ROOTPATH . 'modules/Update/',
|
'Modules\Update' => ROOTPATH . 'modules/Update/',
|
||||||
|
|
@ -57,7 +56,6 @@ class Autoload extends AutoloadConfig
|
||||||
'Themes' => ROOTPATH . 'themes',
|
'Themes' => ROOTPATH . 'themes',
|
||||||
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
|
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
|
||||||
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
|
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
|
||||||
'Vite' => APPPATH . 'Libraries/Vite/',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,7 +93,7 @@ class Autoload extends AutoloadConfig
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
|
public $files = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* -------------------------------------------------------------------
|
* -------------------------------------------------------------------
|
||||||
|
|
@ -108,5 +106,5 @@ class Autoload extends AutoloadConfig
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
public $helpers = ['auth', 'setting', 'icons'];
|
public $helpers = ['auth', 'setting', 'plugins'];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,19 @@ use CodeIgniter\Config\BaseConfig;
|
||||||
|
|
||||||
class CURLRequest extends 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
|
* CURLRequest Share Options
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Cache\CacheInterface;
|
use CodeIgniter\Cache\CacheInterface;
|
||||||
|
use CodeIgniter\Cache\Handlers\ApcuHandler;
|
||||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||||
|
|
@ -36,18 +37,6 @@ class Cache extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string $backupHandler = 'dummy';
|
public string $backupHandler = 'dummy';
|
||||||
|
|
||||||
/**
|
|
||||||
* --------------------------------------------------------------------------
|
|
||||||
* Cache Directory Path
|
|
||||||
* --------------------------------------------------------------------------
|
|
||||||
*
|
|
||||||
* The path to where cache files should be stored, if using a file-based
|
|
||||||
* system.
|
|
||||||
*
|
|
||||||
* @deprecated Use the driver-specific variant under $file
|
|
||||||
*/
|
|
||||||
public string $storePath = WRITEPATH . 'cache/';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* Key Prefix
|
* Key Prefix
|
||||||
|
|
@ -88,10 +77,11 @@ class Cache extends BaseConfig
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* File settings
|
* File settings
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
* Your file storage preferences can be specified below, if you are using
|
* Your file storage preferences can be specified below, if you are using
|
||||||
* the File driver.
|
* the File driver.
|
||||||
*
|
*
|
||||||
* @var array<string, string|int|null>
|
* @var array{storePath?: string, mode?: int}
|
||||||
*/
|
*/
|
||||||
public array $file = [
|
public array $file = [
|
||||||
'storePath' => WRITEPATH . 'cache/',
|
'storePath' => WRITEPATH . 'cache/',
|
||||||
|
|
@ -102,12 +92,13 @@ class Cache extends BaseConfig
|
||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
* Memcached settings
|
* Memcached settings
|
||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
* Your Memcached servers can be specified below, if you are using
|
* Your Memcached servers can be specified below, if you are using
|
||||||
* the Memcached drivers.
|
* the Memcached drivers.
|
||||||
*
|
*
|
||||||
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||||
*
|
*
|
||||||
* @var array<string, string|int|bool>
|
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
|
||||||
*/
|
*/
|
||||||
public array $memcached = [
|
public array $memcached = [
|
||||||
'host' => '127.0.0.1',
|
'host' => '127.0.0.1',
|
||||||
|
|
@ -123,14 +114,24 @@ class Cache extends BaseConfig
|
||||||
* Your Redis server can be specified below, if you are using
|
* Your Redis server can be specified below, if you are using
|
||||||
* the Redis or Predis drivers.
|
* the Redis or Predis drivers.
|
||||||
*
|
*
|
||||||
* @var array<string, string|int|null>
|
* @var array{
|
||||||
|
* host?: string,
|
||||||
|
* password?: string|null,
|
||||||
|
* port?: int,
|
||||||
|
* timeout?: int,
|
||||||
|
* async?: bool,
|
||||||
|
* persistent?: bool,
|
||||||
|
* database?: int
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
public array $redis = [
|
public array $redis = [
|
||||||
'host' => '127.0.0.1',
|
'host' => '127.0.0.1',
|
||||||
'password' => null,
|
'password' => null,
|
||||||
'port' => 6379,
|
'port' => 6379,
|
||||||
'timeout' => 0,
|
'timeout' => 0,
|
||||||
'database' => 0,
|
'async' => false, // specific to Predis and ignored by the native Redis extension
|
||||||
|
'persistent' => false,
|
||||||
|
'database' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,6 +145,7 @@ class Cache extends BaseConfig
|
||||||
* @var array<string, class-string<CacheInterface>>
|
* @var array<string, class-string<CacheInterface>>
|
||||||
*/
|
*/
|
||||||
public array $validHandlers = [
|
public array $validHandlers = [
|
||||||
|
'apcu' => ApcuHandler::class,
|
||||||
'dummy' => DummyHandler::class,
|
'dummy' => DummyHandler::class,
|
||||||
'file' => FileHandler::class,
|
'file' => FileHandler::class,
|
||||||
'memcached' => MemcachedHandler::class,
|
'memcached' => MemcachedHandler::class,
|
||||||
|
|
@ -170,4 +172,28 @@ class Cache extends BaseConfig
|
||||||
* @var bool|list<string>
|
* @var bool|list<string>
|
||||||
*/
|
*/
|
||||||
public $cacheQueryString = false;
|
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.
|
| NOTE: this constant is updated upon release with Continuous Integration.
|
||||||
*/
|
*/
|
||||||
defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
|
defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
| --------------------------------------------------------------------
|
| --------------------------------------------------------------------
|
||||||
|
|
@ -24,10 +24,23 @@ defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
|
||||||
| classes should use.
|
| classes should use.
|
||||||
|
|
|
|
||||||
| NOTE: changing this will require manually modifying the
|
| 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');
|
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
|
| Composer Path
|
||||||
|
|
@ -91,18 +104,3 @@ defined('EXIT_USER_INPUT') || define('EXIT_USER_INPUT', 7); // invalid user inpu
|
||||||
defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error
|
defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error
|
||||||
defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code
|
defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code
|
||||||
defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code
|
defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead.
|
|
||||||
*/
|
|
||||||
define('EVENT_PRIORITY_LOW', 200);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead.
|
|
||||||
*/
|
|
||||||
define('EVENT_PRIORITY_NORMAL', 100);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead.
|
|
||||||
*/
|
|
||||||
define('EVENT_PRIORITY_HIGH', 10);
|
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,24 @@ class ContentSecurityPolicy extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public ?string $reportURI = null;
|
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
|
* 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.
|
* numbers of old URLs that need to be rewritten.
|
||||||
*/
|
*/
|
||||||
public bool $upgradeInsecureRequests = false;
|
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
|
* @var list<string>|string|null
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,6 +56,21 @@ class ContentSecurityPolicy extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string | array $scriptSrc = 'self';
|
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.
|
* Lists allowed stylesheets' URLs.
|
||||||
*
|
*
|
||||||
|
|
@ -53,6 +78,21 @@ class ContentSecurityPolicy extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string | array $styleSrc = 'self';
|
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.
|
* Defines the origins from which images can be loaded.
|
||||||
*
|
*
|
||||||
|
|
@ -132,6 +172,11 @@ class ContentSecurityPolicy extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string | array | null $manifestSrc = null;
|
public string | array | null $manifestSrc = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $workerSrc = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limits the kinds of plugins a page may invoke.
|
* Limits the kinds of plugins a page may invoke.
|
||||||
*
|
*
|
||||||
|
|
@ -147,17 +192,17 @@ class ContentSecurityPolicy extends BaseConfig
|
||||||
public string | array | null $sandbox = null;
|
public string | array | null $sandbox = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nonce tag for style
|
* Nonce placeholder for style tags.
|
||||||
*/
|
*/
|
||||||
public string $styleNonceTag = '{csp-style-nonce}';
|
public string $styleNonceTag = '{csp-style-nonce}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nonce tag for script
|
* Nonce placeholder for script tags.
|
||||||
*/
|
*/
|
||||||
public string $scriptNonceTag = '{csp-script-nonce}';
|
public string $scriptNonceTag = '{csp-script-nonce}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace nonce tag automatically
|
* Replace nonce tag automatically?
|
||||||
*/
|
*/
|
||||||
public bool $autoNonce = true;
|
public bool $autoNonce = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class Cookie extends BaseConfig
|
||||||
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
||||||
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
||||||
*
|
*
|
||||||
* @phpstan-var 'None'|'Lax'|'Strict'|''
|
* @var ''|'Lax'|'None'|'Strict'
|
||||||
*/
|
*/
|
||||||
public string $samesite = 'Lax';
|
public string $samesite = 'Lax';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class Database extends Config
|
||||||
'failover' => [],
|
'failover' => [],
|
||||||
'port' => 3306,
|
'port' => 3306,
|
||||||
'numberNative' => false,
|
'numberNative' => false,
|
||||||
|
'foundRows' => false,
|
||||||
'dateFormat' => [
|
'dateFormat' => [
|
||||||
'date' => 'Y-m-d',
|
'date' => 'Y-m-d',
|
||||||
'datetime' => 'Y-m-d H:i:s',
|
'datetime' => 'Y-m-d H:i:s',
|
||||||
|
|
@ -78,6 +79,7 @@ class Database extends Config
|
||||||
'port' => 3306,
|
'port' => 3306,
|
||||||
'foreignKeys' => true,
|
'foreignKeys' => true,
|
||||||
'busyTimeout' => 1000,
|
'busyTimeout' => 1000,
|
||||||
|
'synchronous' => null,
|
||||||
'dateFormat' => [
|
'dateFormat' => [
|
||||||
'date' => 'Y-m-d',
|
'date' => 'Y-m-d',
|
||||||
'datetime' => 'Y-m-d H:i:s',
|
'datetime' => 'Y-m-d H:i:s',
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
/**
|
|
||||||
* @immutable
|
|
||||||
*/
|
|
||||||
class DocTypes
|
class DocTypes
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ class Email extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string $SMTPHost = '';
|
public string $SMTPHost = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which SMTP authentication method to use: login, plain
|
||||||
|
*/
|
||||||
|
public string $SMTPAuthMethod = 'login';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP Username
|
* SMTP Username
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,23 @@ class Encryption extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public string $key = '';
|
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
|
* Encryption Driver to Use
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,14 @@ Events::on('pre_system', static function (): void {
|
||||||
*/
|
*/
|
||||||
if (CI_DEBUG && ! is_cli()) {
|
if (CI_DEBUG && ! is_cli()) {
|
||||||
Events::on('DBQuery', Database::class . '::collect');
|
Events::on('DBQuery', Database::class . '::collect');
|
||||||
Services::toolbar()->respond();
|
service('toolbar')
|
||||||
|
->respond();
|
||||||
|
|
||||||
// Hot Reload route - for framework use on the hot reloader.
|
// Hot Reload route - for framework use on the hot reloader.
|
||||||
if (ENVIRONMENT === 'development') {
|
if (ENVIRONMENT === 'development') {
|
||||||
Services::routes()->get('__hot-reload', static function (): void {
|
service('routes')->get('__hot-reload', static function (): void {
|
||||||
(new HotReloader())->run();
|
new HotReloader()
|
||||||
|
->run();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,10 @@ class Exceptions extends BaseConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* LOG DEPRECATIONS INSTEAD OF THROWING?
|
* WHETHER TO THROW AN EXCEPTION ON DEPRECATED ERRORS
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* By default, CodeIgniter converts deprecations into exceptions. Also,
|
* If set to `true`, DEPRECATED errors are only logged and no exceptions are
|
||||||
* starting in PHP 8.1 will cause a lot of deprecated usage warnings.
|
* thrown. This option also works for user deprecations.
|
||||||
* Use this option to temporarily cease the warnings and instead log those.
|
|
||||||
* This option also works for user deprecations.
|
|
||||||
*/
|
*/
|
||||||
public bool $logDeprecations = true;
|
public bool $logDeprecations = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ use CodeIgniter\Config\BaseConfig;
|
||||||
class Feature extends BaseConfig
|
class Feature extends BaseConfig
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Use improved new auto routing instead of the default legacy version.
|
* Use improved new auto routing instead of the legacy version.
|
||||||
*/
|
*/
|
||||||
public bool $autoRoutesImproved = false;
|
public bool $autoRoutesImproved = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use filter execution order in 4.4 or before.
|
* Use filter execution order in 4.4 or before.
|
||||||
|
|
@ -28,4 +28,12 @@ class Feature extends BaseConfig
|
||||||
* If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.)
|
* If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.)
|
||||||
*/
|
*/
|
||||||
public bool $limitZeroAsAll = true;
|
public bool $limitZeroAsAll = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use strict location negotiation.
|
||||||
|
*
|
||||||
|
* By default, the locale is selected based on a loose comparison of the language code (ISO 639-1)
|
||||||
|
* Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2).
|
||||||
|
*/
|
||||||
|
public bool $strictLocaleNegotiation = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class Fediverse extends FediverseBaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
['dirname' => $dirname, 'extension' => $extension, 'filename' => $filename] = pathinfo(
|
['dirname' => $dirname, 'extension' => $extension, 'filename' => $filename] = pathinfo(
|
||||||
$defaultBanner['path']
|
$defaultBanner['path'],
|
||||||
);
|
);
|
||||||
$defaultBannerPath = $filename;
|
$defaultBannerPath = $filename;
|
||||||
if ($dirname !== '.') {
|
if ($dirname !== '.') {
|
||||||
|
|
|
||||||
|
|
@ -67,13 +67,20 @@ class Filters extends BaseConfig
|
||||||
/**
|
/**
|
||||||
* List of filter aliases that are always applied before and after every request.
|
* List of filter aliases that are always applied before and after every request.
|
||||||
*
|
*
|
||||||
* @var array<string, array<string, array<string, string|array<string>>>>>|array<string, list<string>>
|
* @var array{
|
||||||
|
* before: array<string, array{except: list<string>|string}>|list<string>,
|
||||||
|
* after: array<string, array{except: list<string>|string}>|list<string>
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
public array $globals = [
|
public array $globals = [
|
||||||
'before' => [
|
'before' => [
|
||||||
// 'honeypot',
|
// 'honeypot',
|
||||||
'csrf' => [
|
'csrf' => [
|
||||||
'except' => ['@[a-zA-Z0-9\_]{1,32}/inbox'],
|
'except' => [
|
||||||
|
'@[a-zA-Z0-9\_]{1,32}/inbox',
|
||||||
|
'api/rest/v1/episodes',
|
||||||
|
'api/rest/v1/episodes/[0-9]+/publish',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
// 'invalidchars',
|
// 'invalidchars',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
use CodeIgniter\Format\FormatterInterface;
|
|
||||||
use CodeIgniter\Format\JSONFormatter;
|
use CodeIgniter\Format\JSONFormatter;
|
||||||
use CodeIgniter\Format\XMLFormatter;
|
use CodeIgniter\Format\XMLFormatter;
|
||||||
|
|
||||||
|
|
@ -65,15 +64,12 @@ class Format extends BaseConfig
|
||||||
'text/xml' => 0,
|
'text/xml' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
//--------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Factory method to return the appropriate formatter for the given mime type.
|
* --------------------------------------------------------------------------
|
||||||
|
* Maximum depth for JSON encoding.
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
*
|
*
|
||||||
* @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
|
* This value determines how deep the JSON encoder will traverse nested structures.
|
||||||
*/
|
*/
|
||||||
public function getFormatter(string $mime): FormatterInterface
|
public int $jsonEncodeDepth = 512;
|
||||||
{
|
|
||||||
return Services::format()->getFormatter($mime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class Generators extends BaseConfig
|
||||||
*
|
*
|
||||||
* YOU HAVE BEEN WARNED!
|
* YOU HAVE BEEN WARNED!
|
||||||
*
|
*
|
||||||
* @var array<string, string>
|
* @var array<string, string|array<string,string>>
|
||||||
*/
|
*/
|
||||||
public array $views = [
|
public array $views = [
|
||||||
'make:cell' => [
|
'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.
|
* 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';
|
public string $libraryPath = '/usr/local/bin/convert';
|
||||||
|
|
||||||
|
|
@ -130,7 +132,7 @@ class Images extends BaseConfig
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
public string $avatarDefaultPath = 'castopod-avatar.jpg';
|
public string $avatarDefaultPath = 'assets/images/castopod-avatar.jpg';
|
||||||
|
|
||||||
public string $avatarDefaultMimeType = 'image/jpg';
|
public string $avatarDefaultMimeType = 'image/jpg';
|
||||||
|
|
||||||
|
|
@ -139,31 +141,31 @@ class Images extends BaseConfig
|
||||||
*/
|
*/
|
||||||
public array $podcastBannerDefaultPaths = [
|
public array $podcastBannerDefaultPaths = [
|
||||||
'default' => [
|
'default' => [
|
||||||
'path' => 'castopod-banner-pine.jpg',
|
'path' => 'assets/images/castopod-banner-pine.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'pine' => [
|
'pine' => [
|
||||||
'path' => 'castopod-banner-pine.jpg',
|
'path' => 'assets/images/castopod-banner-pine.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'crimson' => [
|
'crimson' => [
|
||||||
'path' => 'castopod-banner-crimson.jpg',
|
'path' => 'assets/images/castopod-banner-crimson.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'amber' => [
|
'amber' => [
|
||||||
'path' => 'castopod-banner-amber.jpg',
|
'path' => 'assets/images/castopod-banner-amber.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'lake' => [
|
'lake' => [
|
||||||
'path' => 'castopod-banner-lake.jpg',
|
'path' => 'assets/images/castopod-banner-lake.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'jacaranda' => [
|
'jacaranda' => [
|
||||||
'path' => 'castopod-banner-jacaranda.jpg',
|
'path' => 'assets/images/castopod-banner-jacaranda.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
'onyx' => [
|
'onyx' => [
|
||||||
'path' => 'castopod-banner-onyx.jpg',
|
'path' => 'assets/images/castopod-banner-onyx.jpg',
|
||||||
'mimetype' => 'image/jpeg',
|
'mimetype' => 'image/jpeg',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use Kint\Parser\ConstructablePluginInterface;
|
use Kint\Parser\ConstructablePluginInterface;
|
||||||
use Kint\Renderer\AbstractRenderer;
|
|
||||||
use Kint\Renderer\Rich\TabPluginInterface;
|
use Kint\Renderer\Rich\TabPluginInterface;
|
||||||
use Kint\Renderer\Rich\ValuePluginInterface;
|
use Kint\Renderer\Rich\ValuePluginInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* Kint
|
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
*
|
*
|
||||||
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
|
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
|
||||||
|
|
@ -48,8 +46,6 @@ class Kint
|
||||||
|
|
||||||
public bool $richFolder = false;
|
public bool $richFolder = false;
|
||||||
|
|
||||||
public int $richSort = AbstractRenderer::SORT_FULL;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<string, class-string<ValuePluginInterface>>|null
|
* @var array<string, class-string<ValuePluginInterface>>|null
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
use CodeIgniter\Log\Handlers\FileHandler;
|
use CodeIgniter\Log\Handlers\FileHandler;
|
||||||
|
use CodeIgniter\Log\Handlers\HandlerInterface;
|
||||||
|
|
||||||
class Logger extends BaseConfig
|
class Logger extends BaseConfig
|
||||||
{
|
{
|
||||||
|
|
@ -75,7 +76,7 @@ class Logger extends BaseConfig
|
||||||
* Handlers are executed in the order defined in this array, starting with
|
* Handlers are executed in the order defined in this array, starting with
|
||||||
* the handler on top and continuing down.
|
* the handler on top and continuing down.
|
||||||
*
|
*
|
||||||
* @var array<class-string, array<string, int|list<string>|string>>
|
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
|
||||||
*/
|
*/
|
||||||
public array $handlers = [
|
public array $handlers = [
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,19 @@ class Migrations extends BaseConfig
|
||||||
* - Y_m_d_His_
|
* - Y_m_d_His_
|
||||||
*/
|
*/
|
||||||
public string $timestampFormat = '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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mimes
|
|
||||||
*
|
|
||||||
* This file contains an array of mime types. It is used by the Upload class to help identify allowed file types.
|
* This file contains an array of mime types. It is used by the Upload class to help identify allowed file types.
|
||||||
*
|
*
|
||||||
* When more than one variation for an extension exist (like jpg, jpeg, etc) the most common one should be first in the
|
* When more than one variation for an extension exist (like jpg, jpeg, etc) the most common one should be first in the
|
||||||
|
|
@ -14,8 +12,6 @@ namespace Config;
|
||||||
*
|
*
|
||||||
* When working with mime types, please make sure you have the ´fileinfo´ extension enabled to reliably detect the
|
* When working with mime types, please make sure you have the ´fileinfo´ extension enabled to reliably detect the
|
||||||
* media types.
|
* media types.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Mimes
|
class Mimes
|
||||||
{
|
{
|
||||||
|
|
@ -283,7 +279,8 @@ class Mimes
|
||||||
'srt' => ['application/x-subrip', 'text/srt', 'text/plain', 'application/octet-stream'],
|
'srt' => ['application/x-subrip', 'text/srt', 'text/plain', 'application/octet-stream'],
|
||||||
'vtt' => ['text/vtt', 'text/plain'],
|
'vtt' => ['text/vtt', 'text/plain'],
|
||||||
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
|
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
|
||||||
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle'],
|
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle', 'model/stl',
|
||||||
|
'application/octet-stream', ],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -310,7 +307,7 @@ class Mimes
|
||||||
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
|
* @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.
|
* @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), '. ');
|
$type = trim(strtolower($type), '. ');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ use CodeIgniter\Modules\Modules as BaseModules;
|
||||||
*
|
*
|
||||||
* NOTE: This class is required prior to Autoloader instantiation,
|
* NOTE: This class is required prior to Autoloader instantiation,
|
||||||
* and does not extend BaseConfig.
|
* and does not extend BaseConfig.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Modules extends BaseModules
|
class Modules extends BaseModules
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace Config;
|
||||||
* NOTE: This class does not extend BaseConfig for performance reasons.
|
* NOTE: This class does not extend BaseConfig for performance reasons.
|
||||||
* So you cannot replace the property values with Environment Variables.
|
* So you cannot replace the property values with Environment Variables.
|
||||||
*
|
*
|
||||||
* @immutable
|
* WARNING: Do not use these options when running the app in the Worker Mode.
|
||||||
*/
|
*/
|
||||||
class Optimize
|
class Optimize
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paths
|
|
||||||
*
|
|
||||||
* Holds the paths that are used by the system to locate the main directories, app, system, etc.
|
* Holds the paths that are used by the system to locate the main directories, app, system, etc.
|
||||||
*
|
*
|
||||||
* Modifying these allows you to restructure your application, share a system folder between multiple applications, and
|
* Modifying these allows you to restructure your application, share a system folder between multiple applications, and
|
||||||
* more.
|
* more.
|
||||||
*
|
*
|
||||||
* All paths are relative to the project's root folder.
|
* All paths are relative to the project's root folder.
|
||||||
|
*
|
||||||
|
* NOTE: This class is required prior to Autoloader instantiation,
|
||||||
|
* and does not extend BaseConfig.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Paths
|
class Paths
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,7 +72,7 @@ class Paths
|
||||||
* This variable must contain the name of the directory that
|
* This variable must contain the name of the directory that
|
||||||
* contains the view files used by your application. By
|
* contains the view files used by your application. By
|
||||||
* default this is in `app/Views`. This value
|
* default this is in `app/Views`. This value
|
||||||
* is used when no value is provided to `Services::renderer()`.
|
* is used when no value is provided to `service('renderer')`.
|
||||||
*/
|
*/
|
||||||
public string $viewDirectory = __DIR__ . '/../Views';
|
public string $viewDirectory = __DIR__ . '/../Views';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ declare(strict_types=1);
|
||||||
use CodeIgniter\Router\RouteCollection;
|
use CodeIgniter\Router\RouteCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var RouteCollection $routes
|
|
||||||
*
|
|
||||||
* --------------------------------------------------------------------
|
* --------------------------------------------------------------------
|
||||||
* Placeholder definitions
|
* Placeholder definitions
|
||||||
* --------------------------------------------------------------------
|
* --------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
/** @var RouteCollection $routes */
|
||||||
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
|
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
|
||||||
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
|
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
|
||||||
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
|
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
|
||||||
|
|
@ -161,7 +159,7 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
||||||
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
|
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
|
||||||
'as' => 'episode-comment-replies',
|
'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',
|
'as' => 'episode-comment-attempt-like',
|
||||||
]);
|
]);
|
||||||
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
|
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
|
||||||
|
|
@ -229,9 +227,9 @@ $routes->get('/pages/(:slug)', 'PageController::index/$1', [
|
||||||
* Overwriting Fediverse routes file
|
* Overwriting Fediverse routes file
|
||||||
*/
|
*/
|
||||||
$routes->group('@(:podcastHandle)', static function ($routes): void {
|
$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',
|
'as' => 'post-attempt-create',
|
||||||
'filter' => 'permission:podcast#.manage-publications',
|
'filter' => 'permission:podcast$1.manage-publications',
|
||||||
]);
|
]);
|
||||||
// Post
|
// Post
|
||||||
$routes->group('posts/(:uuid)', static function ($routes): void {
|
$routes->group('posts/(:uuid)', static function ($routes): void {
|
||||||
|
|
@ -266,13 +264,13 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
||||||
'filter' => 'allow-cors',
|
'filter' => 'allow-cors',
|
||||||
]);
|
]);
|
||||||
// Actions
|
// Actions
|
||||||
$routes->post('action', 'PostController::attemptAction/$1/$2', [
|
$routes->post('action', 'PostController::action/$1/$2', [
|
||||||
'as' => 'post-attempt-action',
|
'as' => 'post-attempt-action',
|
||||||
'filter' => 'permission:podcast#.interact-as',
|
'filter' => 'permission:podcast$1.interact-as',
|
||||||
]);
|
]);
|
||||||
$routes->post(
|
$routes->post(
|
||||||
'block-actor',
|
'block-actor',
|
||||||
'PostController::attemptBlockActor/$1/$2',
|
'PostController::blockActorAction/$1/$2',
|
||||||
[
|
[
|
||||||
'as' => 'post-attempt-block-actor',
|
'as' => 'post-attempt-block-actor',
|
||||||
'filter' => 'permission:fediverse.manage-blocks',
|
'filter' => 'permission:fediverse.manage-blocks',
|
||||||
|
|
@ -280,25 +278,25 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
||||||
);
|
);
|
||||||
$routes->post(
|
$routes->post(
|
||||||
'block-domain',
|
'block-domain',
|
||||||
'PostController::attemptBlockDomain/$1/$2',
|
'PostController::blockDomainAction/$1/$2',
|
||||||
[
|
[
|
||||||
'as' => 'post-attempt-block-domain',
|
'as' => 'post-attempt-block-domain',
|
||||||
'filter' => 'permission:fediverse.manage-blocks',
|
'filter' => 'permission:fediverse.manage-blocks',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
$routes->post('delete', 'PostController::attemptDelete/$1/$2', [
|
$routes->post('delete', 'PostController::deleteAction/$1/$2', [
|
||||||
'as' => 'post-attempt-delete',
|
'as' => 'post-attempt-delete',
|
||||||
'filter' => 'permission:podcast#.manage-publications',
|
'filter' => 'permission:podcast$1.manage-publications',
|
||||||
]);
|
]);
|
||||||
$routes->get(
|
$routes->get(
|
||||||
'remote/(:postAction)',
|
'remote/(:postAction)',
|
||||||
'PostController::remoteAction/$1/$2/$3',
|
'PostController::remoteActionAction/$1/$2/$3',
|
||||||
[
|
[
|
||||||
'as' => 'post-remote-action',
|
'as' => 'post-remote-action',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
$routes->get('follow', 'ActorController::follow/$1', [
|
$routes->get('follow', 'ActorController::followView/$1', [
|
||||||
'as' => 'follow',
|
'as' => 'follow',
|
||||||
]);
|
]);
|
||||||
$routes->get('outbox', 'ActorController::outbox/$1', [
|
$routes->get('outbox', 'ActorController::outbox/$1', [
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,15 @@ class Routing extends BaseRouting
|
||||||
*/
|
*/
|
||||||
public bool $autoRoute = false;
|
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.
|
* For Defined Routes.
|
||||||
* If TRUE, will enable the use of the 'prioritize' option
|
* If TRUE, will enable the use of the 'prioritize' option
|
||||||
|
|
@ -138,5 +147,5 @@ class Routing extends BaseRouting
|
||||||
*
|
*
|
||||||
* Default: false
|
* Default: false
|
||||||
*/
|
*/
|
||||||
public bool $translateUriToCamelCase = false;
|
public bool $translateUriToCamelCase = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,23 +83,4 @@ class Security extends BaseConfig
|
||||||
* @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure
|
* @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure
|
||||||
*/
|
*/
|
||||||
public bool $redirect = (ENVIRONMENT === 'production');
|
public bool $redirect = (ENVIRONMENT === 'production');
|
||||||
|
|
||||||
/**
|
|
||||||
* --------------------------------------------------------------------------
|
|
||||||
* CSRF SameSite
|
|
||||||
* --------------------------------------------------------------------------
|
|
||||||
*
|
|
||||||
* Setting for CSRF SameSite cookie token.
|
|
||||||
*
|
|
||||||
* Allowed values are: None - Lax - Strict - ''.
|
|
||||||
*
|
|
||||||
* Defaults to `Lax` as recommended in this link:
|
|
||||||
*
|
|
||||||
* @see https://portswigger.net/web-security/csrf/samesite-cookies
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*
|
|
||||||
* @deprecated `Config\Cookie` $samesite property is used.
|
|
||||||
*/
|
|
||||||
public $samesite = 'Lax';
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ declare(strict_types=1);
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use App\Libraries\Breadcrumb;
|
use App\Libraries\Breadcrumb;
|
||||||
|
use App\Libraries\HtmlHead;
|
||||||
use App\Libraries\Negotiate;
|
use App\Libraries\Negotiate;
|
||||||
use App\Libraries\Router;
|
use App\Libraries\Router;
|
||||||
use CodeIgniter\Config\BaseService;
|
use CodeIgniter\Config\BaseService;
|
||||||
|
use CodeIgniter\HTTP\Negotiate as CodeIgniterHTTPNegotiate;
|
||||||
use CodeIgniter\HTTP\Request;
|
use CodeIgniter\HTTP\Request;
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\Router\RouteCollectionInterface;
|
use CodeIgniter\Router\RouteCollectionInterface;
|
||||||
|
use CodeIgniter\Router\Router as CodeIgniterRouter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services Configuration file.
|
* Services Configuration file.
|
||||||
|
|
@ -31,8 +34,8 @@ class Services extends BaseService
|
||||||
public static function router(
|
public static function router(
|
||||||
?RouteCollectionInterface $routes = null,
|
?RouteCollectionInterface $routes = null,
|
||||||
?Request $request = null,
|
?Request $request = null,
|
||||||
bool $getShared = true
|
bool $getShared = true,
|
||||||
): Router {
|
): CodeIgniterRouter {
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
return static::getSharedInstance('router', $routes, $request);
|
return static::getSharedInstance('router', $routes, $request);
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +50,10 @@ class Services extends BaseService
|
||||||
* The Negotiate class provides the content negotiation features for working the request to determine correct
|
* The Negotiate class provides the content negotiation features for working the request to determine correct
|
||||||
* language, encoding, charset, and more.
|
* language, encoding, charset, and more.
|
||||||
*/
|
*/
|
||||||
public static function negotiator(?RequestInterface $request = null, bool $getShared = true): Negotiate
|
public static function negotiator(
|
||||||
{
|
?RequestInterface $request = null,
|
||||||
|
bool $getShared = true,
|
||||||
|
): CodeIgniterHTTPNegotiate {
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
return static::getSharedInstance('negotiator', $request);
|
return static::getSharedInstance('negotiator', $request);
|
||||||
}
|
}
|
||||||
|
|
@ -66,4 +71,13 @@ class Services extends BaseService
|
||||||
|
|
||||||
return new Breadcrumb();
|
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:
|
* The session storage driver to use:
|
||||||
|
* - `CodeIgniter\Session\Handlers\ArrayHandler` (for testing)
|
||||||
* - `CodeIgniter\Session\Handlers\FileHandler`
|
* - `CodeIgniter\Session\Handlers\FileHandler`
|
||||||
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
||||||
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
|
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
|
||||||
|
|
|
||||||
|
|
@ -51,5 +51,9 @@ class Tasks extends BaseConfig
|
||||||
$schedule->command('podcast:import')
|
$schedule->command('podcast:import')
|
||||||
->everyMinute()
|
->everyMinute()
|
||||||
->named('podcast-import');
|
->named('podcast-import');
|
||||||
|
|
||||||
|
$schedule->command('episodes:compute-downloads')
|
||||||
|
->everyHour()
|
||||||
|
->named('episodes:compute-downloads');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,4 +117,29 @@ class Toolbar extends BaseConfig
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env'];
|
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 = [
|
public array $robots = [
|
||||||
'googlebot' => 'Googlebot',
|
'googlebot' => 'Googlebot',
|
||||||
|
'google-pagerenderer' => 'Google Page Renderer',
|
||||||
|
'google-read-aloud' => 'Google Read Aloud',
|
||||||
|
'google-safety' => 'Google Safety Bot',
|
||||||
'msnbot' => 'MSNBot',
|
'msnbot' => 'MSNBot',
|
||||||
'baiduspider' => 'Baiduspider',
|
'baiduspider' => 'Baiduspider',
|
||||||
'bingbot' => 'Bing',
|
'bingbot' => 'Bing',
|
||||||
|
'bingpreview' => 'BingPreview',
|
||||||
'slurp' => 'Inktomi Slurp',
|
'slurp' => 'Inktomi Slurp',
|
||||||
'yahoo' => 'Yahoo',
|
'yahoo' => 'Yahoo',
|
||||||
'ask jeeves' => 'Ask Jeeves',
|
'ask jeeves' => 'Ask Jeeves',
|
||||||
|
|
@ -250,5 +254,11 @@ class UserAgents extends BaseConfig
|
||||||
'ia_archiver' => 'Alexa Crawler',
|
'ia_archiver' => 'Alexa Crawler',
|
||||||
'MJ12bot' => 'Majestic-12',
|
'MJ12bot' => 'Majestic-12',
|
||||||
'Uptimebot' => 'Uptimebot',
|
'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;
|
namespace Config;
|
||||||
|
|
||||||
use App\Validation\FileRules as AppFileRules;
|
use App\Validation\FileRules as AppFileRules;
|
||||||
|
use App\Validation\OtherRules;
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
use CodeIgniter\Validation\StrictRules\CreditCardRules;
|
use CodeIgniter\Validation\StrictRules\CreditCardRules;
|
||||||
use CodeIgniter\Validation\StrictRules\FileRules;
|
use CodeIgniter\Validation\StrictRules\FileRules;
|
||||||
|
|
@ -24,6 +25,7 @@ class Validation extends BaseConfig
|
||||||
FileRules::class,
|
FileRules::class,
|
||||||
CreditCardRules::class,
|
CreditCardRules::class,
|
||||||
AppFileRules::class,
|
AppFileRules::class,
|
||||||
|
OtherRules::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,21 @@ class View extends BaseView
|
||||||
* @var list<class-string<ViewDecoratorInterface>>
|
* @var list<class-string<ViewDecoratorInterface>>
|
||||||
*/
|
*/
|
||||||
public array $decorators = [Decorator::class];
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
app/Config/Vite.php
Normal file
42
app/Config/Vite.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// app/Config/Vite.php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
use CodeIgniterVite\Config\Vite as ViteConfig;
|
||||||
|
|
||||||
|
class Vite extends ViteConfig
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$adminGateway = config('Admin')
|
||||||
|
->gateway;
|
||||||
|
$installGateway = config('Install')
|
||||||
|
->gateway;
|
||||||
|
|
||||||
|
$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'];
|
protected $helpers = ['svg', 'components', 'misc', 'seo'];
|
||||||
|
|
||||||
public function follow(): string
|
public function followView(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
// @phpstan-ignore-next-line
|
||||||
if (! auth()->loggedIn()) {
|
$this->registerPodcastWebpageHit($this->actor->podcast->id);
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
$this->registerPodcastWebpageHit($this->actor->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
helper(['form', 'components', 'svg']);
|
helper(['form', 'components', 'svg']);
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
set_follow_metatags($this->actor);
|
||||||
$data = [
|
$data = [
|
||||||
// @phpstan-ignore-next-line
|
'actor' => $this->actor,
|
||||||
'metatags' => get_follow_metatags($this->actor),
|
|
||||||
'actor' => $this->actor,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('podcast/follow', $data);
|
return view('podcast/follow', $data);
|
||||||
|
|
|
||||||
|
|
@ -5,35 +5,25 @@ declare(strict_types=1);
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
use CodeIgniter\HTTP\IncomingRequest;
|
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\HTTP\Response;
|
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use Override;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use ViewThemes\Theme;
|
use ViewThemes\Theme;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BaseController provides a convenient place for loading components and performing functions that are needed by all
|
* 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
|
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
|
* An array of helpers to be loaded automatically upon
|
||||||
* class instantiation. These helpers will be available
|
* class instantiation. These helpers will be available
|
||||||
|
|
@ -49,14 +39,14 @@ abstract class BaseController extends Controller
|
||||||
*/
|
*/
|
||||||
// protected $session;
|
// protected $session;
|
||||||
|
|
||||||
/**
|
#[Override]
|
||||||
* Constructor.
|
|
||||||
*/
|
|
||||||
public function initController(
|
public function initController(
|
||||||
RequestInterface $request,
|
RequestInterface $request,
|
||||||
ResponseInterface $response,
|
ResponseInterface $response,
|
||||||
LoggerInterface $logger
|
LoggerInterface $logger,
|
||||||
): void {
|
): 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'];
|
$this->helpers = [...$this->helpers, 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
|
||||||
|
|
||||||
// Do Not Edit This Line
|
// Do Not Edit This Line
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,11 @@ declare(strict_types=1);
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
use CodeIgniter\HTTP\Response;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
class ColorsController extends Controller
|
class ColorsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
public function index(): ResponseInterface
|
||||||
* Instance of the main response object.
|
|
||||||
*
|
|
||||||
* @var Response
|
|
||||||
*/
|
|
||||||
protected $response;
|
|
||||||
|
|
||||||
public function index(): Response
|
|
||||||
{
|
{
|
||||||
$cacheName = 'colors.css';
|
$cacheName = 'colors.css';
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ class CreditsController extends BaseController
|
||||||
'content_markdown' => '',
|
'content_markdown' => '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$allPodcasts = (new PodcastModel())->findAll();
|
$allPodcasts = new PodcastModel()
|
||||||
$allCredits = (new CreditModel())->findAll();
|
->findAll();
|
||||||
|
$allCredits = new CreditModel()
|
||||||
|
->findAll();
|
||||||
|
|
||||||
// Unlike the carpenter, we make a tree from a table:
|
// Unlike the carpenter, we make a tree from a table:
|
||||||
$personGroup = null;
|
$personGroup = null;
|
||||||
|
|
@ -164,10 +166,10 @@ class CreditsController extends BaseController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_page_metatags($page);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_page_metatags($page),
|
'page' => $page,
|
||||||
'page' => $page,
|
'credits' => $credits,
|
||||||
'credits' => $credits,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$found = view('pages/credits', $data);
|
$found = view('pages/credits', $data);
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,18 @@ use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\HTTP\IncomingRequest;
|
|
||||||
use CodeIgniter\HTTP\RedirectResponse;
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use CodeIgniter\HTTP\URI;
|
use CodeIgniter\HTTP\URI;
|
||||||
use Config\Services;
|
|
||||||
use Modules\Analytics\Config\Analytics;
|
use Modules\Analytics\Config\Analytics;
|
||||||
use Modules\PremiumPodcasts\Entities\Subscription;
|
use Modules\PremiumPodcasts\Entities\Subscription;
|
||||||
use Modules\PremiumPodcasts\Models\SubscriptionModel;
|
use Modules\PremiumPodcasts\Models\SubscriptionModel;
|
||||||
|
use Override;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class EpisodeAudioController extends Controller
|
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
|
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
|
||||||
* other controllers that extend Analytics.
|
* other controllers that extend Analytics.
|
||||||
|
|
@ -50,13 +42,11 @@ class EpisodeAudioController extends Controller
|
||||||
|
|
||||||
protected Analytics $analyticsConfig;
|
protected Analytics $analyticsConfig;
|
||||||
|
|
||||||
/**
|
#[Override]
|
||||||
* Constructor.
|
|
||||||
*/
|
|
||||||
public function initController(
|
public function initController(
|
||||||
RequestInterface $request,
|
RequestInterface $request,
|
||||||
ResponseInterface $response,
|
ResponseInterface $response,
|
||||||
LoggerInterface $logger
|
LoggerInterface $logger,
|
||||||
): void {
|
): void {
|
||||||
// Do Not Edit This Line
|
// Do Not Edit This Line
|
||||||
parent::initController($request, $response, $logger);
|
parent::initController($request, $response, $logger);
|
||||||
|
|
@ -75,7 +65,7 @@ class EpisodeAudioController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +73,7 @@ class EpisodeAudioController extends Controller
|
||||||
$this->podcast = $podcast;
|
$this->podcast = $podcast;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +93,7 @@ class EpisodeAudioController extends Controller
|
||||||
|
|
||||||
// check if podcast is already unlocked before any token validation
|
// check if podcast is already unlocked before any token validation
|
||||||
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
|
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
|
||||||
$this->episode->podcast->handle
|
$this->episode->podcast->handle,
|
||||||
)) instanceof Subscription) {
|
)) instanceof Subscription) {
|
||||||
// look for token as GET parameter
|
// look for token as GET parameter
|
||||||
if (($token = $this->request->getGet('token')) === null) {
|
if (($token = $this->request->getGet('token')) === null) {
|
||||||
|
|
@ -118,9 +108,9 @@ class EpisodeAudioController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there's a valid subscription for the provided token
|
// 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,
|
$this->episode->podcast->handle,
|
||||||
$token
|
$token,
|
||||||
)) instanceof Subscription) {
|
)) instanceof Subscription) {
|
||||||
return $this->response->setStatusCode(401, 'Invalid token!')
|
return $this->response->setStatusCode(401, 'Invalid token!')
|
||||||
->setJSON([
|
->setJSON([
|
||||||
|
|
@ -133,7 +123,7 @@ class EpisodeAudioController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$session = Services::session();
|
$session = service('session');
|
||||||
|
|
||||||
$serviceName = '';
|
$serviceName = '';
|
||||||
if ($this->request->getGet('_from')) {
|
if ($this->request->getGet('_from')) {
|
||||||
|
|
@ -164,7 +154,7 @@ class EpisodeAudioController extends Controller
|
||||||
$audioDuration,
|
$audioDuration,
|
||||||
$this->episode->published_at->getTimestamp(),
|
$this->episode->published_at->getTimestamp(),
|
||||||
$serviceName,
|
$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));
|
$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 App\Models\PodcastModel;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\HTTP\RedirectResponse;
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
use CodeIgniter\HTTP\Response;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Entities\Actor;
|
use Modules\Fediverse\Entities\Actor;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||||
|
|
@ -44,7 +44,7 @@ class EpisodeCommentController extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ class EpisodeCommentController extends BaseController
|
||||||
$this->actor = $podcast->actor;
|
$this->actor = $podcast->actor;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +61,7 @@ class EpisodeCommentController extends BaseController
|
||||||
$this->episode = $episode;
|
$this->episode = $episode;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($comment = (new EpisodeCommentModel())->getCommentById($params[2])) instanceof EpisodeComment
|
! ($comment = new EpisodeCommentModel()->getCommentById($params[2])) instanceof EpisodeComment
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -77,10 +77,7 @@ class EpisodeCommentController extends BaseController
|
||||||
|
|
||||||
public function view(): string
|
public function view(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -96,12 +93,12 @@ class EpisodeCommentController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
|
set_episode_comment_metatags($this->comment);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_episode_comment_metatags($this->comment),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'actor' => $this->actor,
|
||||||
'actor' => $this->actor,
|
'episode' => $this->episode,
|
||||||
'episode' => $this->episode,
|
'comment' => $this->comment,
|
||||||
'comment' => $this->comment,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// if user is logged in then send to the authenticated activity view
|
// if user is logged in then send to the authenticated activity view
|
||||||
|
|
@ -119,7 +116,7 @@ class EpisodeCommentController extends BaseController
|
||||||
return $cachedView;
|
return $cachedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function commentObject(): Response
|
public function commentObject(): ResponseInterface
|
||||||
{
|
{
|
||||||
$commentObject = new CommentObject($this->comment);
|
$commentObject = new CommentObject($this->comment);
|
||||||
|
|
||||||
|
|
@ -128,7 +125,7 @@ class EpisodeCommentController extends BaseController
|
||||||
->setBody($commentObject->toJSON());
|
->setBody($commentObject->toJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replies(): Response
|
public function replies(): ResponseInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* get comment replies
|
* get comment replies
|
||||||
|
|
@ -148,11 +145,9 @@ class EpisodeCommentController extends BaseController
|
||||||
$pager = $commentReplies->pager;
|
$pager = $commentReplies->pager;
|
||||||
|
|
||||||
$orderedItems = [];
|
$orderedItems = [];
|
||||||
if ($paginatedReplies !== null) {
|
foreach ($paginatedReplies as $reply) {
|
||||||
foreach ($paginatedReplies as $reply) {
|
$replyObject = new CommentObject($reply);
|
||||||
$replyObject = new CommentObject($reply);
|
$orderedItems[] = $replyObject;
|
||||||
$orderedItems[] = $replyObject;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||||
|
|
@ -163,7 +158,7 @@ class EpisodeCommentController extends BaseController
|
||||||
->setBody($collection->toJSON());
|
->setBody($collection->toJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptLike(): RedirectResponse
|
public function likeAction(): RedirectResponse
|
||||||
{
|
{
|
||||||
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
|
|
@ -175,7 +170,7 @@ class EpisodeCommentController extends BaseController
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptReply(): RedirectResponse
|
public function replyAction(): RedirectResponse
|
||||||
{
|
{
|
||||||
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,8 @@ use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\Database\BaseBuilder;
|
use CodeIgniter\Database\BaseBuilder;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\HTTP\Response;
|
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Config\Embed;
|
use Config\Embed;
|
||||||
use Config\Services;
|
|
||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||||
|
|
@ -43,7 +41,7 @@ class EpisodeController extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +49,7 @@ class EpisodeController extends BaseController
|
||||||
$this->podcast = $podcast;
|
$this->podcast = $podcast;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -66,10 +64,7 @@ class EpisodeController extends BaseController
|
||||||
|
|
||||||
public function index(): string
|
public function index(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -86,15 +81,14 @@ class EpisodeController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
|
set_episode_metatags($this->episode);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_episode_metatags($this->episode),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'episode' => $this->episode,
|
||||||
'episode' => $this->episode,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
if (auth()->loggedIn()) {
|
if (auth()->loggedIn()) {
|
||||||
helper('form');
|
helper('form');
|
||||||
|
|
@ -114,10 +108,7 @@ class EpisodeController extends BaseController
|
||||||
|
|
||||||
public function activity(): string
|
public function activity(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -135,15 +126,14 @@ class EpisodeController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
|
set_episode_metatags($this->episode);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_episode_metatags($this->episode),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'episode' => $this->episode,
|
||||||
'episode' => $this->episode,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
if (auth()->loggedIn()) {
|
if (auth()->loggedIn()) {
|
||||||
helper('form');
|
helper('form');
|
||||||
|
|
@ -163,10 +153,7 @@ class EpisodeController extends BaseController
|
||||||
|
|
||||||
public function chapters(): string
|
public function chapters(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -184,13 +171,13 @@ class EpisodeController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
// get chapters from json file
|
set_episode_metatags($this->episode);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_episode_metatags($this->episode),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'episode' => $this->episode,
|
||||||
'episode' => $this->episode,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// get chapters from json file
|
||||||
if (isset($this->episode->chapters->file_key)) {
|
if (isset($this->episode->chapters->file_key)) {
|
||||||
/** @var FileManagerInterface $fileManager */
|
/** @var FileManagerInterface $fileManager */
|
||||||
$fileManager = service('file_manager');
|
$fileManager = service('file_manager');
|
||||||
|
|
@ -200,9 +187,8 @@ class EpisodeController extends BaseController
|
||||||
$data['chapters'] = $chapters;
|
$data['chapters'] = $chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
if (auth()->loggedIn()) {
|
if (auth()->loggedIn()) {
|
||||||
helper('form');
|
helper('form');
|
||||||
|
|
@ -222,10 +208,7 @@ class EpisodeController extends BaseController
|
||||||
|
|
||||||
public function transcript(): string
|
public function transcript(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -243,13 +226,13 @@ class EpisodeController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
// get transcript from json file
|
set_episode_metatags($this->episode);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_episode_metatags($this->episode),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'episode' => $this->episode,
|
||||||
'episode' => $this->episode,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// get transcript from json file
|
||||||
if ($this->episode->transcript !== null) {
|
if ($this->episode->transcript !== null) {
|
||||||
$data['transcript'] = $this->episode->transcript;
|
$data['transcript'] = $this->episode->transcript;
|
||||||
|
|
||||||
|
|
@ -257,16 +240,15 @@ class EpisodeController extends BaseController
|
||||||
/** @var FileManagerInterface $fileManager */
|
/** @var FileManagerInterface $fileManager */
|
||||||
$fileManager = service('file_manager');
|
$fileManager = service('file_manager');
|
||||||
$transcriptJsonString = (string) $fileManager->getFileContents(
|
$transcriptJsonString = (string) $fileManager->getFileContents(
|
||||||
$this->episode->transcript->json_key
|
$this->episode->transcript->json_key,
|
||||||
);
|
);
|
||||||
|
|
||||||
$data['captions'] = json_decode($transcriptJsonString, true);
|
$data['captions'] = json_decode($transcriptJsonString, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
if (auth()->loggedIn()) {
|
if (auth()->loggedIn()) {
|
||||||
helper('form');
|
helper('form');
|
||||||
|
|
@ -288,12 +270,9 @@ class EpisodeController extends BaseController
|
||||||
{
|
{
|
||||||
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
||||||
|
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$session = Services::session();
|
$session = service('session');
|
||||||
|
|
||||||
if (service('superglobals')->server('HTTP_REFERER') !== null) {
|
if (service('superglobals')->server('HTTP_REFERER') !== null) {
|
||||||
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
|
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
|
||||||
|
|
@ -323,9 +302,8 @@ class EpisodeController extends BaseController
|
||||||
'themeData' => $themeData,
|
'themeData' => $themeData,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||||
return view('embed', $data, [
|
return view('embed', $data, [
|
||||||
|
|
@ -382,7 +360,7 @@ class EpisodeController extends BaseController
|
||||||
'<iframe src="' .
|
'<iframe src="' .
|
||||||
$this->episode->embed_url .
|
$this->episode->embed_url .
|
||||||
'" width="100%" height="' . config(
|
'" width="100%" height="' . config(
|
||||||
Embed::class
|
Embed::class,
|
||||||
)->height . '" frameborder="0" scrolling="no"></iframe>',
|
)->height . '" frameborder="0" scrolling="no"></iframe>',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -393,7 +371,7 @@ class EpisodeController extends BaseController
|
||||||
return $this->response->setXML($oembed);
|
return $this->response->setXML($oembed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function episodeObject(): Response
|
public function episodeObject(): ResponseInterface
|
||||||
{
|
{
|
||||||
$podcastObject = new PodcastEpisode($this->episode);
|
$podcastObject = new PodcastEpisode($this->episode);
|
||||||
|
|
||||||
|
|
@ -402,7 +380,7 @@ class EpisodeController extends BaseController
|
||||||
->setBody($podcastObject->toJSON());
|
->setBody($podcastObject->toJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function comments(): Response
|
public function comments(): ResponseInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* get comments: aggregated replies from posts referring to the episode
|
* get comments: aggregated replies from posts referring to the episode
|
||||||
|
|
@ -425,10 +403,8 @@ class EpisodeController extends BaseController
|
||||||
$pager = $episodeComments->pager;
|
$pager = $episodeComments->pager;
|
||||||
|
|
||||||
$orderedItems = [];
|
$orderedItems = [];
|
||||||
if ($paginatedComments !== null) {
|
foreach ($paginatedComments as $comment) {
|
||||||
foreach ($paginatedComments as $comment) {
|
$orderedItems[] = new NoteObject($comment)->toArray();
|
||||||
$orderedItems[] = (new NoteObject($comment))->toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ class EpisodePreviewController extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
// find episode by previewUUID
|
// find episode by previewUUID
|
||||||
$episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
|
$episode = new EpisodeModel()
|
||||||
|
->getEpisodeByPreviewId($params[0]);
|
||||||
|
|
||||||
if (! $episode instanceof Episode) {
|
if (! $episode instanceof Episode) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
|
@ -99,7 +100,7 @@ class EpisodePreviewController extends BaseController
|
||||||
/** @var FileManagerInterface $fileManager */
|
/** @var FileManagerInterface $fileManager */
|
||||||
$fileManager = service('file_manager');
|
$fileManager = service('file_manager');
|
||||||
$transcriptJsonString = (string) $fileManager->getFileContents(
|
$transcriptJsonString = (string) $fileManager->getFileContents(
|
||||||
$this->episode->transcript->json_key
|
$this->episode->transcript->json_key,
|
||||||
);
|
);
|
||||||
|
|
||||||
$data['captions'] = json_decode($transcriptJsonString, true);
|
$data['captions'] = json_decode($transcriptJsonString, true);
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,26 @@ class FeedController extends Controller
|
||||||
|
|
||||||
public function index(string $podcastHandle): ResponseInterface
|
public function index(string $podcastHandle): ResponseInterface
|
||||||
{
|
{
|
||||||
helper(['rss', 'premium_podcasts', 'misc']);
|
$podcast = new PodcastModel()
|
||||||
|
->where('handle', $podcastHandle)
|
||||||
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
|
|
||||||
->first();
|
->first();
|
||||||
if (! $podcast instanceof Podcast) {
|
if (! $podcast instanceof Podcast) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 301 redirect to new feed?
|
||||||
|
$redirectToNewFeed = service('settings')
|
||||||
|
->get('Podcast.redirect_to_new_feed', 'podcast:' . $podcast->id);
|
||||||
|
|
||||||
|
if ($redirectToNewFeed && $podcast->new_feed_url !== null && filter_var(
|
||||||
|
$podcast->new_feed_url,
|
||||||
|
FILTER_VALIDATE_URL,
|
||||||
|
) && $podcast->new_feed_url !== current_url()) {
|
||||||
|
return redirect()->to($podcast->new_feed_url, 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
helper(['rss', 'premium_podcasts', 'misc']);
|
||||||
|
|
||||||
$service = null;
|
$service = null;
|
||||||
try {
|
try {
|
||||||
$service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT'));
|
$service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT'));
|
||||||
|
|
@ -57,7 +69,8 @@ class FeedController extends Controller
|
||||||
$subscription = null;
|
$subscription = null;
|
||||||
$token = $this->request->getGet('token');
|
$token = $this->request->getGet('token');
|
||||||
if ($token) {
|
if ($token) {
|
||||||
$subscription = (new SubscriptionModel())->validateSubscription($podcastHandle, $token);
|
$subscription = new SubscriptionModel()
|
||||||
|
->validateSubscription($podcastHandle, $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
|
|
@ -66,7 +79,7 @@ class FeedController extends Controller
|
||||||
"podcast#{$podcast->id}",
|
"podcast#{$podcast->id}",
|
||||||
'feed',
|
'feed',
|
||||||
$service ? $serviceSlug : null,
|
$service ? $serviceSlug : null,
|
||||||
$subscription instanceof Subscription ? 'unlocked' : null,
|
$subscription instanceof Subscription ? "subscription#{$subscription->id}" : null,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -74,9 +87,8 @@ class FeedController extends Controller
|
||||||
$found = get_rss_feed($podcast, $serviceSlug, $subscription, $token);
|
$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
|
// 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(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$podcast->id,
|
->getSecondsToNextUnpublishedEpisode($podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
cache()
|
cache()
|
||||||
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
|
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,20 @@ class HomeController extends BaseController
|
||||||
{
|
{
|
||||||
$sortOptions = ['activity', 'created_desc', 'created_asc'];
|
$sortOptions = ['activity', 'created_desc', 'created_asc'];
|
||||||
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
|
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
|
||||||
'sort'
|
'sort',
|
||||||
) : 'activity';
|
) : 'activity';
|
||||||
|
|
||||||
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
|
$allPodcasts = new PodcastModel()
|
||||||
|
->getAllPodcasts($sortBy);
|
||||||
|
|
||||||
// check if there's only one podcast to redirect user to it
|
// check if there's only one podcast to redirect user to it
|
||||||
if (count($allPodcasts) === 1) {
|
if (count($allPodcasts) === 1) {
|
||||||
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
|
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_home_metatags();
|
||||||
// default behavior: list all podcasts on home page
|
// default behavior: list all podcasts on home page
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_home_metatags(),
|
|
||||||
'podcasts' => $allPodcasts,
|
'podcasts' => $allPodcasts,
|
||||||
'sortBy' => $sortBy,
|
'sortBy' => $sortBy,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ class MapController extends BaseController
|
||||||
{
|
{
|
||||||
$cacheName = 'episodes_markers';
|
$cacheName = 'episodes_markers';
|
||||||
if (! ($found = cache($cacheName))) {
|
if (! ($found = cache($cacheName))) {
|
||||||
$episodes = (new EpisodeModel())
|
$episodes = new EpisodeModel()
|
||||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||||
->where('location_geo is not', null)
|
->where('location_geo is not')
|
||||||
->findAll();
|
->findAll();
|
||||||
$found = [];
|
$found = [];
|
||||||
foreach ($episodes as $episode) {
|
foreach ($episodes as $episode) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ class PageController extends BaseController
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
$page = (new PageModel())->where('slug', $params[0])->first();
|
$page = new PageModel()
|
||||||
|
->where('slug', $params[0])->first();
|
||||||
if (! $page instanceof Page) {
|
if (! $page instanceof Page) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -49,9 +50,9 @@ class PageController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($found = cache($cacheName))) {
|
if (! ($found = cache($cacheName))) {
|
||||||
|
set_page_metatags($this->page);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_page_metatags($this->page),
|
'page' => $this->page,
|
||||||
'page' => $this->page,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$found = view('pages/page', $data);
|
$found = view('pages/page', $data);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use App\Models\PostModel;
|
use App\Models\PostModel;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\HTTP\Response;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||||
|
|
@ -35,7 +35,7 @@ class PodcastController extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ class PodcastController extends BaseController
|
||||||
return $this->{$method}(...$params);
|
return $this->{$method}(...$params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function podcastActor(): Response
|
public function podcastActor(): ResponseInterface
|
||||||
{
|
{
|
||||||
$podcastActor = new PodcastActor($this->podcast);
|
$podcastActor = new PodcastActor($this->podcast);
|
||||||
|
|
||||||
|
|
@ -58,10 +58,7 @@ class PodcastController extends BaseController
|
||||||
|
|
||||||
public function activity(): string
|
public function activity(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -78,10 +75,11 @@ class PodcastController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
|
set_podcast_metatags($this->podcast, 'activity');
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_podcast_metatags($this->podcast, 'activity'),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'posts' => new PostModel()
|
||||||
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
|
->getActorPublishedPosts($this->podcast->actor_id),
|
||||||
];
|
];
|
||||||
|
|
||||||
// if user is logged in then send to the authenticated activity view
|
// 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);
|
return view('podcast/activity', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
return view('podcast/activity', $data, [
|
return view('podcast/activity', $data, [
|
||||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||||
|
|
@ -106,10 +103,7 @@ class PodcastController extends BaseController
|
||||||
|
|
||||||
public function about(): string
|
public function about(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -126,12 +120,13 @@ class PodcastController extends BaseController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
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 = [
|
$data = [
|
||||||
'metatags' => get_podcast_metatags($this->podcast, 'about'),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'stats' => $stats,
|
||||||
'stats' => $stats,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// // if user is logged in then send to the authenticated activity view
|
// // 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);
|
return view('podcast/about', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
|
|
||||||
return view('podcast/about', $data, [
|
return view('podcast/about', $data, [
|
||||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||||
|
|
@ -156,16 +150,14 @@ class PodcastController extends BaseController
|
||||||
|
|
||||||
public function episodes(): string
|
public function episodes(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$yearQuery = $this->request->getGet('year');
|
$yearQuery = $this->request->getGet('year');
|
||||||
$seasonQuery = $this->request->getGet('season');
|
$seasonQuery = $this->request->getGet('season');
|
||||||
|
|
||||||
if (! $yearQuery && ! $seasonQuery) {
|
if (! $yearQuery && ! $seasonQuery) {
|
||||||
$defaultQuery = (new PodcastModel())->getDefaultQuery($this->podcast->id);
|
$defaultQuery = new PodcastModel()
|
||||||
|
->getDefaultQuery($this->podcast->id);
|
||||||
if ($defaultQuery) {
|
if ($defaultQuery) {
|
||||||
if ($defaultQuery['type'] === 'season') {
|
if ($defaultQuery['type'] === 'season') {
|
||||||
$seasonQuery = $defaultQuery['data']['season_number'];
|
$seasonQuery = $defaultQuery['data']['season_number'];
|
||||||
|
|
@ -245,26 +237,21 @@ class PodcastController extends BaseController
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_podcast_metatags($this->podcast, 'episodes');
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_podcast_metatags($this->podcast, 'episodes'),
|
|
||||||
'podcast' => $this->podcast,
|
'podcast' => $this->podcast,
|
||||||
'episodesNav' => $episodesNavigation,
|
'episodesNav' => $episodesNavigation,
|
||||||
'activeQuery' => $activeQuery,
|
'activeQuery' => $activeQuery,
|
||||||
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
|
'episodes' => new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getPodcastEpisodes($this->podcast->id, $this->podcast->type, $yearQuery, $seasonQuery),
|
||||||
$this->podcast->type,
|
|
||||||
$yearQuery,
|
|
||||||
$seasonQuery,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (auth()->loggedIn()) {
|
if (auth()->loggedIn()) {
|
||||||
return view('podcast/episodes', $data);
|
return view('podcast/episodes', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
$this->podcast->id,
|
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
|
||||||
);
|
|
||||||
return view('podcast/episodes', $data, [
|
return view('podcast/episodes', $data, [
|
||||||
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
|
||||||
'cache_name' => $cacheName,
|
'cache_name' => $cacheName,
|
||||||
|
|
@ -274,7 +261,7 @@ class PodcastController extends BaseController
|
||||||
return $cachedView;
|
return $cachedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function episodeCollection(): Response
|
public function episodeCollection(): ResponseInterface
|
||||||
{
|
{
|
||||||
if ($this->podcast->type === 'serial') {
|
if ($this->podcast->type === 'serial') {
|
||||||
// podcast is serial
|
// podcast is serial
|
||||||
|
|
@ -298,10 +285,8 @@ class PodcastController extends BaseController
|
||||||
$pager = $episodes->pager;
|
$pager = $episodes->pager;
|
||||||
|
|
||||||
$orderedItems = [];
|
$orderedItems = [];
|
||||||
if ($paginatedEpisodes !== null) {
|
foreach ($paginatedEpisodes as $episode) {
|
||||||
foreach ($paginatedEpisodes as $episode) {
|
$orderedItems[] = new PodcastEpisode($episode)->toArray();
|
||||||
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
|
|
@ -315,9 +300,9 @@ class PodcastController extends BaseController
|
||||||
|
|
||||||
public function links(): string
|
public function links(): string
|
||||||
{
|
{
|
||||||
|
set_podcast_metatags($this->podcast, 'links');
|
||||||
return view('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 CodeIgniter\I18n\Time;
|
||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Controllers\PostController as FediversePostController;
|
use Modules\Fediverse\Controllers\PostController as FediversePostController;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class PostController extends FediversePostController
|
class PostController extends FediversePostController
|
||||||
{
|
{
|
||||||
|
|
@ -41,10 +42,12 @@ class PostController extends FediversePostController
|
||||||
*/
|
*/
|
||||||
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
|
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function _remap(string $method, string ...$params): mixed
|
public function _remap(string $method, string ...$params): mixed
|
||||||
{
|
{
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
@ -52,29 +55,34 @@ class PostController extends FediversePostController
|
||||||
$this->podcast = $podcast;
|
$this->podcast = $podcast;
|
||||||
$this->actor = $this->podcast->actor;
|
$this->actor = $this->podcast->actor;
|
||||||
|
|
||||||
if (
|
if (count($params) <= 1) {
|
||||||
count($params) > 1 &&
|
|
||||||
($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
|
|
||||||
) {
|
|
||||||
$this->post = $post;
|
|
||||||
|
|
||||||
unset($params[0]);
|
unset($params[0]);
|
||||||
unset($params[1]);
|
|
||||||
|
return $this->{$method}(...$params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
! ($post = new PostModel()->getPostById($params[1])) instanceof CastopodPost
|
||||||
|
) {
|
||||||
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->post = $post;
|
||||||
|
|
||||||
|
// show 404 if post is private
|
||||||
|
if ($this->post->is_private && ! can_user_interact()) {
|
||||||
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($params[0]);
|
||||||
|
unset($params[1]);
|
||||||
|
|
||||||
return $this->{$method}(...$params);
|
return $this->{$method}(...$params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view(): string
|
public function view(): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->post instanceof CastopodPost) {
|
|
||||||
throw PageNotFoundException::forPageNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -89,10 +97,10 @@ class PostController extends FediversePostController
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
|
set_post_metatags($this->post);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_post_metatags($this->post),
|
'post' => $this->post,
|
||||||
'post' => $this->post,
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// if user is logged in then send to the authenticated activity view
|
// if user is logged in then send to the authenticated activity view
|
||||||
|
|
@ -110,7 +118,8 @@ class PostController extends FediversePostController
|
||||||
return $cachedView;
|
return $cachedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptCreate(): RedirectResponse
|
#[Override]
|
||||||
|
public function createAction(): RedirectResponse
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'message' => 'required|max_length[500]',
|
'message' => 'required|max_length[500]',
|
||||||
|
|
@ -139,7 +148,7 @@ class PostController extends FediversePostController
|
||||||
if (
|
if (
|
||||||
$episodeUri &&
|
$episodeUri &&
|
||||||
($params = extract_params_from_episode_uri(new URI($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;
|
$newPost->episode_id = $episode->id;
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +170,8 @@ class PostController extends FediversePostController
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptReply(): RedirectResponse
|
#[Override]
|
||||||
|
public function replyAction(): RedirectResponse
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'message' => 'required|max_length[500]',
|
'message' => 'required|max_length[500]',
|
||||||
|
|
@ -180,6 +190,7 @@ class PostController extends FediversePostController
|
||||||
'actor_id' => interact_as_actor_id(),
|
'actor_id' => interact_as_actor_id(),
|
||||||
'in_reply_to_id' => $this->post->id,
|
'in_reply_to_id' => $this->post->id,
|
||||||
'message' => $validData['message'],
|
'message' => $validData['message'],
|
||||||
|
'is_private' => $this->post->is_private,
|
||||||
'published_at' => Time::now(),
|
'published_at' => Time::now(),
|
||||||
'created_by' => user_id(),
|
'created_by' => user_id(),
|
||||||
]);
|
]);
|
||||||
|
|
@ -200,21 +211,24 @@ class PostController extends FediversePostController
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptFavourite(): RedirectResponse
|
#[Override]
|
||||||
|
public function favouriteAction(): RedirectResponse
|
||||||
{
|
{
|
||||||
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
|
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
|
||||||
|
|
||||||
return redirect()->back();
|
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();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attemptAction(): RedirectResponse
|
public function action(): RedirectResponse
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'action' => 'required|in_list[favourite,reblog,reply]',
|
'action' => 'required|in_list[favourite,reblog,reply]',
|
||||||
|
|
@ -231,9 +245,9 @@ class PostController extends FediversePostController
|
||||||
|
|
||||||
$action = $validData['action'];
|
$action = $validData['action'];
|
||||||
return match ($action) {
|
return match ($action) {
|
||||||
'favourite' => $this->attemptFavourite(),
|
'favourite' => $this->favouriteAction(),
|
||||||
'reblog' => $this->attemptReblog(),
|
'reblog' => $this->reblogAction(),
|
||||||
'reply' => $this->attemptReply(),
|
'reply' => $this->replyAction(),
|
||||||
default => redirect()
|
default => redirect()
|
||||||
->back()
|
->back()
|
||||||
->withInput()
|
->withInput()
|
||||||
|
|
@ -241,19 +255,16 @@ class PostController extends FediversePostController
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remoteAction(string $action): string
|
public function remoteActionView(string $action): string
|
||||||
{
|
{
|
||||||
// Prevent analytics hit when authenticated
|
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||||
if (! auth()->loggedIn()) {
|
|
||||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
set_remote_actions_metatags($this->post, $action);
|
||||||
$data = [
|
$data = [
|
||||||
'metatags' => get_remote_actions_metatags($this->post, $action),
|
'podcast' => $this->podcast,
|
||||||
'podcast' => $this->podcast,
|
'actor' => $this->actor,
|
||||||
'actor' => $this->actor,
|
'post' => $this->post,
|
||||||
'post' => $this->post,
|
'action' => $action,
|
||||||
'action' => $action,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
helper('form');
|
helper('form');
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class WebmanifestController extends Controller
|
||||||
/**
|
/**
|
||||||
* @var array<string, array<string, string>>
|
* @var array<string, array<string, string>>
|
||||||
*/
|
*/
|
||||||
final public const THEME_COLORS = [
|
final public const array THEME_COLORS = [
|
||||||
'pine' => [
|
'pine' => [
|
||||||
'theme' => '#009486',
|
'theme' => '#009486',
|
||||||
'background' => '#F0F9F8',
|
'background' => '#F0F9F8',
|
||||||
|
|
@ -82,7 +82,7 @@ class WebmanifestController extends Controller
|
||||||
public function podcastManifest(string $podcastHandle): ResponseInterface
|
public function podcastManifest(string $podcastHandle): ResponseInterface
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) instanceof Podcast
|
! ($podcast = new PodcastModel()->getPodcastByHandle($podcastHandle)) instanceof Podcast
|
||||||
) {
|
) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddCategories extends BaseMigration
|
class AddCategories extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -45,6 +48,7 @@ class AddCategories extends BaseMigration
|
||||||
$this->forge->createTable('categories');
|
$this->forge->createTable('categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('categories');
|
$this->forge->dropTable('categories');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddLanguages extends BaseMigration
|
class AddLanguages extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -31,6 +34,7 @@ class AddLanguages extends BaseMigration
|
||||||
$this->forge->createTable('languages');
|
$this->forge->createTable('languages');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('languages');
|
$this->forge->dropTable('languages');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcasts extends BaseMigration
|
class AddPodcasts extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -205,6 +208,7 @@ class AddPodcasts extends BaseMigration
|
||||||
$this->forge->createTable('podcasts');
|
$this->forge->createTable('podcasts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('podcasts');
|
$this->forge->dropTable('podcasts');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddEpisodes extends BaseMigration
|
class AddEpisodes extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -171,6 +174,7 @@ class AddEpisodes extends BaseMigration
|
||||||
$this->db->query($createQuery);
|
$this->db->query($createQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('episodes');
|
$this->forge->dropTable('episodes');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPlatforms extends BaseMigration
|
class AddPlatforms extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$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('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
|
||||||
$this->forge->addField(
|
$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->addPrimaryKey('slug');
|
||||||
$this->forge->createTable('platforms');
|
$this->forge->createTable('platforms');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('platforms');
|
$this->forge->dropTable('platforms');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsPlatforms extends BaseMigration
|
class AddPodcastsPlatforms extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -52,6 +55,7 @@ class AddPodcastsPlatforms extends BaseMigration
|
||||||
$this->forge->createTable('podcasts_platforms');
|
$this->forge->createTable('podcasts_platforms');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('podcasts_platforms');
|
$this->forge->dropTable('podcasts_platforms');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddEpisodeComments extends BaseMigration
|
class AddEpisodeComments extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -71,6 +74,7 @@ class AddEpisodeComments extends BaseMigration
|
||||||
$this->forge->createTable('episode_comments');
|
$this->forge->createTable('episode_comments');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('episode_comments');
|
$this->forge->dropTable('episode_comments');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddLikes extends BaseMigration
|
class AddLikes extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -34,6 +37,7 @@ class AddLikes extends BaseMigration
|
||||||
$this->forge->createTable('likes');
|
$this->forge->createTable('likes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('likes');
|
$this->forge->dropTable('likes');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPages extends BaseMigration
|
class AddPages extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -48,6 +51,7 @@ class AddPages extends BaseMigration
|
||||||
$this->forge->createTable('pages');
|
$this->forge->createTable('pages');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('pages');
|
$this->forge->dropTable('pages');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsCategories extends BaseMigration
|
class AddPodcastsCategories extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -32,6 +35,7 @@ class AddPodcastsCategories extends BaseMigration
|
||||||
$this->forge->createTable('podcasts_categories');
|
$this->forge->createTable('podcasts_categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('podcasts_categories');
|
$this->forge->dropTable('podcasts_categories');
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddClips extends BaseMigration
|
class AddClips extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -94,6 +97,7 @@ class AddClips extends BaseMigration
|
||||||
$this->forge->createTable('clips');
|
$this->forge->createTable('clips');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('clips');
|
$this->forge->dropTable('clips');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPersons extends BaseMigration
|
class AddPersons extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -67,6 +70,7 @@ class AddPersons extends BaseMigration
|
||||||
$this->forge->createTable('persons');
|
$this->forge->createTable('persons');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('persons');
|
$this->forge->dropTable('persons');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsPersons extends BaseMigration
|
class AddPodcastsPersons extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -46,6 +49,7 @@ class AddPodcastsPersons extends BaseMigration
|
||||||
$this->forge->createTable('podcasts_persons');
|
$this->forge->createTable('podcasts_persons');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('podcasts_persons');
|
$this->forge->dropTable('podcasts_persons');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddEpisodesPersons extends BaseMigration
|
class AddEpisodesPersons extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -51,6 +54,7 @@ class AddEpisodesPersons extends BaseMigration
|
||||||
$this->forge->createTable('episodes_persons');
|
$this->forge->createTable('episodes_persons');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('episodes_persons');
|
$this->forge->dropTable('episodes_persons');
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddCreditsView extends BaseMigration
|
class AddCreditsView extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
// Creates View for credit UNION query
|
// Creates View for credit UNION query
|
||||||
|
|
@ -37,6 +40,7 @@ class AddCreditsView extends BaseMigration
|
||||||
$this->db->query($createQuery);
|
$this->db->query($createQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$viewName = $this->db->prefixTable('credits');
|
$viewName = $this->db->prefixTable('credits');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddEpisodeIdToPosts extends BaseMigration
|
class AddEpisodeIdToPosts extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
@ -33,11 +36,12 @@ class AddEpisodeIdToPosts extends BaseMigration
|
||||||
'id',
|
'id',
|
||||||
'',
|
'',
|
||||||
'CASCADE',
|
'CASCADE',
|
||||||
$prefix . 'fediverse_posts_episode_id_foreign'
|
$prefix . 'fediverse_posts_episode_id_foreign',
|
||||||
);
|
);
|
||||||
$this->forge->processIndexes('fediverse_posts');
|
$this->forge->processIndexes('fediverse_posts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddCreatedByToPosts extends BaseMigration
|
class AddCreatedByToPosts extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
@ -33,11 +36,12 @@ class AddCreatedByToPosts extends BaseMigration
|
||||||
'id',
|
'id',
|
||||||
'',
|
'',
|
||||||
'CASCADE',
|
'CASCADE',
|
||||||
$prefix . 'fediverse_posts_created_by_foreign'
|
$prefix . 'fediverse_posts_created_by_foreign',
|
||||||
);
|
);
|
||||||
$this->forge->processIndexes('fediverse_posts');
|
$this->forge->processIndexes('fediverse_posts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddFullTextSearchIndexes extends BaseMigration
|
class AddFullTextSearchIndexes extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
@ -31,6 +34,7 @@ class AddFullTextSearchIndexes extends BaseMigration
|
||||||
$this->db->query($createQuery);
|
$this->db->query($createQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddEpisodePreviewId extends BaseMigration
|
class AddEpisodePreviewId extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$fields = [
|
$fields = [
|
||||||
|
|
@ -28,6 +31,7 @@ class AddEpisodePreviewId extends BaseMigration
|
||||||
$this->db->query($uniquePreviewId);
|
$this->db->query($uniquePreviewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$fields = ['preview_id'];
|
$fields = ['preview_id'];
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
|
class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$fields = [
|
$fields = [
|
||||||
|
|
@ -28,6 +31,7 @@ class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
|
||||||
$this->forge->addColumn('podcasts', $fields);
|
$this->forge->addColumn('podcasts', $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$fields = ['is_owner_email_removed_from_feed'];
|
$fields = ['is_owner_email_removed_from_feed'];
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsMediumField extends BaseMigration
|
class AddPodcastsMediumField extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$fields = [
|
$fields = [
|
||||||
|
|
@ -28,6 +31,7 @@ class AddPodcastsMediumField extends BaseMigration
|
||||||
$this->forge->addColumn('podcasts', $fields);
|
$this->forge->addColumn('podcasts', $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$fields = ['medium'];
|
$fields = ['medium'];
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AddPodcastsVerifyTxtField extends BaseMigration
|
class AddPodcastsVerifyTxtField extends BaseMigration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$fields = [
|
$fields = [
|
||||||
|
|
@ -27,6 +30,7 @@ class AddPodcastsVerifyTxtField extends BaseMigration
|
||||||
$this->forge->addColumn('podcasts', $fields);
|
$this->forge->addColumn('podcasts', $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$this->forge->dropColumn('podcasts', 'verify_txt');
|
$this->forge->dropColumn('podcasts', 'verify_txt');
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class RefactorPlatforms extends Migration
|
class RefactorPlatforms extends Migration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
|
|
@ -80,6 +82,7 @@ class RefactorPlatforms extends Migration
|
||||||
$this->forge->renameTable('platforms_temp', 'platforms');
|
$this->forge->renameTable('platforms_temp', 'platforms');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
// delete platforms
|
// delete platforms
|
||||||
|
|
@ -111,7 +114,7 @@ class RefactorPlatforms extends Migration
|
||||||
]);
|
]);
|
||||||
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
|
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
|
||||||
$this->forge->addField(
|
$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->addPrimaryKey('slug');
|
||||||
$this->forge->createTable('platforms');
|
$this->forge->createTable('platforms');
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
use Override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CodeIgniter 4.5.1 introduces new DataCaster class that breaks deserialization of import queue tasks.
|
* 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
|
class ClearImportQueue extends Migration
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
service('settings')->forget('Import.queue');
|
service('settings')->forget('Import.queue');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
// nothing
|
// nothing
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddEpisodeDownloadsCount extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
'downloads_count' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'unsigned' => true,
|
||||||
|
'default' => 0,
|
||||||
|
'after' => 'is_published_on_hubs',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->forge->addColumn('episodes', $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->forge->dropColumn('episodes', 'downloads_count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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\BaseConnection;
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class BaseMigration extends Migration
|
class BaseMigration extends Migration
|
||||||
{
|
{
|
||||||
|
|
@ -24,10 +25,12 @@ class BaseMigration extends Migration
|
||||||
*/
|
*/
|
||||||
protected $db;
|
protected $db;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Seeds;
|
namespace App\Database\Seeds;
|
||||||
|
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AppSeeder extends Seeder
|
class AppSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call('CategorySeeder');
|
$this->call('CategorySeeder');
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Seeds;
|
namespace App\Database\Seeds;
|
||||||
|
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class CategorySeeder extends Seeder
|
class CategorySeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Seeds;
|
namespace App\Database\Seeds;
|
||||||
|
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class DevSeeder extends Seeder
|
class DevSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call('CategorySeeder');
|
$this->call('CategorySeeder');
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@ namespace App\Database\Seeds;
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
use CodeIgniter\Shield\Entities\User;
|
use CodeIgniter\Shield\Entities\User;
|
||||||
use Modules\Auth\Models\UserModel;
|
use Modules\Auth\Models\UserModel;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class DevSuperadminSeeder extends Seeder
|
class DevSuperadminSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ use CodeIgniter\Database\Seeder;
|
||||||
use Exception;
|
use Exception;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use GeoIp2\Exception\AddressNotFoundException;
|
use GeoIp2\Exception\AddressNotFoundException;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class FakePodcastsAnalyticsSeeder extends Seeder
|
class FakePodcastsAnalyticsSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$jsonUserAgents = json_decode(
|
$jsonUserAgents = json_decode(
|
||||||
|
|
@ -41,13 +43,14 @@ class FakePodcastsAnalyticsSeeder extends Seeder
|
||||||
JSON_THROW_ON_ERROR,
|
JSON_THROW_ON_ERROR,
|
||||||
);
|
);
|
||||||
|
|
||||||
$podcast = (new PodcastModel())->first();
|
$podcast = new PodcastModel()
|
||||||
|
->first();
|
||||||
|
|
||||||
if (! $podcast instanceof Podcast) {
|
if (! $podcast instanceof Podcast) {
|
||||||
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
$firstEpisode = (new EpisodeModel())
|
$firstEpisode = new EpisodeModel()
|
||||||
->selectMin('published_at')
|
->selectMin('published_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
|
@ -67,7 +70,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
|
||||||
$analyticsPodcastsByPlayer = [];
|
$analyticsPodcastsByPlayer = [];
|
||||||
$analyticsPodcastsByRegion = [];
|
$analyticsPodcastsByRegion = [];
|
||||||
|
|
||||||
$episodes = (new EpisodeModel())
|
$episodes = new EpisodeModel()
|
||||||
->where('podcast_id', $podcast->id)
|
->where('podcast_id', $podcast->id)
|
||||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class FakeWebsiteAnalyticsSeeder extends Seeder
|
class FakeWebsiteAnalyticsSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|
@ -181,15 +182,17 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
|
||||||
'WOSBrowser',
|
'WOSBrowser',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$podcast = (new PodcastModel())->first();
|
$podcast = new PodcastModel()
|
||||||
|
->first();
|
||||||
|
|
||||||
if (! $podcast instanceof Podcast) {
|
if (! $podcast instanceof Podcast) {
|
||||||
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
$firstEpisode = (new EpisodeModel())
|
$firstEpisode = new EpisodeModel()
|
||||||
->selectMin('published_at')
|
->selectMin('published_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
|
@ -206,7 +209,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
|
||||||
$websiteByEntryPage = [];
|
$websiteByEntryPage = [];
|
||||||
$websiteByReferer = [];
|
$websiteByReferer = [];
|
||||||
|
|
||||||
$episodes = (new EpisodeModel())
|
$episodes = new EpisodeModel()
|
||||||
->where('podcast_id', $podcast->id)
|
->where('podcast_id', $podcast->id)
|
||||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ declare(strict_types=1);
|
||||||
namespace App\Database\Seeds;
|
namespace App\Database\Seeds;
|
||||||
|
|
||||||
use CodeIgniter\Database\Seeder;
|
use CodeIgniter\Database\Seeder;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class LanguageSeeder extends Seeder
|
class LanguageSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
#[Override]
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace App\Entities;
|
||||||
|
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use Modules\Fediverse\Entities\Actor as FediverseActor;
|
use Modules\Fediverse\Entities\Actor as FediverseActor;
|
||||||
use RuntimeException;
|
use Override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Podcast|null $podcast
|
* @property Podcast|null $podcast
|
||||||
|
|
@ -31,17 +31,15 @@ class Actor extends FediverseActor
|
||||||
|
|
||||||
public function getPodcast(): ?Podcast
|
public function getPodcast(): ?Podcast
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast id must be set before getting associated podcast.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->podcast instanceof Podcast) {
|
if (! $this->podcast instanceof Podcast) {
|
||||||
$this->podcast = (new PodcastModel())->getPodcastByActorId($this->id);
|
$this->podcast = new PodcastModel()
|
||||||
|
->getPodcastByActorId($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->podcast;
|
return $this->podcast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function getAvatarImageUrl(): string
|
public function getAvatarImageUrl(): string
|
||||||
{
|
{
|
||||||
if ($this->podcast instanceof Podcast) {
|
if ($this->podcast instanceof Podcast) {
|
||||||
|
|
@ -51,6 +49,7 @@ class Actor extends FediverseActor
|
||||||
return parent::getAvatarImageUrl();
|
return parent::getAvatarImageUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function getAvatarImageMimetype(): string
|
public function getAvatarImageMimetype(): string
|
||||||
{
|
{
|
||||||
if ($this->podcast instanceof Podcast) {
|
if ($this->podcast instanceof Podcast) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use CodeIgniter\Entity\Entity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $parent_id
|
* @property ?int $parent_id
|
||||||
* @property Category|null $parent
|
* @property Category|null $parent
|
||||||
* @property string $code
|
* @property string $code
|
||||||
* @property string $apple_category
|
* @property string $apple_category
|
||||||
|
|
@ -42,6 +42,7 @@ class Category extends Entity
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new CategoryModel())->getCategoryById($this->parent_id);
|
return new CategoryModel()
|
||||||
|
->getCategoryById($this->parent_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ use Modules\Media\Models\MediaModel;
|
||||||
* @property Episode $episode
|
* @property Episode $episode
|
||||||
* @property string $title
|
* @property string $title
|
||||||
* @property double $start_time
|
* @property double $start_time
|
||||||
* @property double $end_time
|
* @property ?double $end_time
|
||||||
* @property double $duration
|
* @property double $duration
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property int|null $media_id
|
* @property int|null $media_id
|
||||||
* @property Video|Audio|null $media
|
* @property Video|Audio|null $media
|
||||||
* @property array|null $metadata
|
* @property array<mixed>|null $metadata
|
||||||
* @property string $status
|
* @property string $status
|
||||||
* @property string $logs
|
* @property string $logs
|
||||||
* @property User $user
|
* @property User $user
|
||||||
|
|
@ -81,14 +81,6 @@ class BaseClip extends Entity
|
||||||
'updated_by' => 'integer',
|
'updated_by' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed>|null $data
|
|
||||||
*/
|
|
||||||
public function __construct(array $data = null)
|
|
||||||
{
|
|
||||||
parent::__construct($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getJobDuration(): ?int
|
public function getJobDuration(): ?int
|
||||||
{
|
{
|
||||||
if ($this->job_duration === null && $this->job_started_at && $this->job_ended_at) {
|
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
|
public function getPodcast(): ?Podcast
|
||||||
{
|
{
|
||||||
return (new PodcastModel())->getPodcastById($this->podcast_id);
|
return new PodcastModel()
|
||||||
|
->getPodcastById($this->podcast_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEpisode(): ?Episode
|
public function getEpisode(): ?Episode
|
||||||
{
|
{
|
||||||
return (new EpisodeModel())->getEpisodeById($this->episode_id);
|
return new EpisodeModel()
|
||||||
|
->getEpisodeById($this->episode_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(): ?User
|
public function getUser(): ?User
|
||||||
{
|
{
|
||||||
/** @var ?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
|
public function setMedia(File $file, string $fileKey): static
|
||||||
|
|
@ -131,18 +126,19 @@ class BaseClip extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getMedia()
|
$this->getMedia()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('audio'))->updateMedia($this->getMedia());
|
new MediaModel('audio')
|
||||||
|
->updateMedia($this->getMedia());
|
||||||
} else {
|
} else {
|
||||||
$media = new Audio([
|
$media = new Audio([
|
||||||
'file_key' => $fileKey,
|
'file_key' => $fileKey,
|
||||||
'language_code' => $this->getPodcast()
|
'language_code' => $this->getPodcast()
|
||||||
->language_code,
|
->language_code,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$media->setFile($file);
|
$media->setFile($file);
|
||||||
|
|
||||||
$this->attributes['media_id'] = (new MediaModel())->saveMedia($media);
|
$this->attributes['media_id'] = new MediaModel()->saveMedia($media);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -151,7 +147,8 @@ class BaseClip extends Entity
|
||||||
public function getMedia(): Audio | Video | null
|
public function getMedia(): Audio | Video | null
|
||||||
{
|
{
|
||||||
if ($this->media_id !== null && $this->media === 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;
|
return $this->media;
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,10 @@ namespace App\Entities\Clip;
|
||||||
use CodeIgniter\Files\File;
|
use CodeIgniter\Files\File;
|
||||||
use Modules\Media\Entities\Video;
|
use Modules\Media\Entities\Video;
|
||||||
use Modules\Media\Models\MediaModel;
|
use Modules\Media\Models\MediaModel;
|
||||||
|
use Override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property array $theme
|
* @property array{name:string,preview:string} $theme
|
||||||
* @property string $format
|
* @property string $format
|
||||||
*/
|
*/
|
||||||
class VideoClip extends BaseClip
|
class VideoClip extends BaseClip
|
||||||
|
|
@ -25,7 +26,7 @@ class VideoClip extends BaseClip
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed>|null $data
|
* @param array<string, mixed>|null $data
|
||||||
*/
|
*/
|
||||||
public function __construct(array $data = null)
|
public function __construct(?array $data = null)
|
||||||
{
|
{
|
||||||
parent::__construct($data);
|
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
|
public function setTheme(array $theme): self
|
||||||
{
|
{
|
||||||
|
|
@ -63,6 +64,7 @@ class VideoClip extends BaseClip
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function setMedia(File $file, string $fileKey): static
|
public function setMedia(File $file, string $fileKey): static
|
||||||
{
|
{
|
||||||
if ($this->attributes['media_id'] !== null) {
|
if ($this->attributes['media_id'] !== null) {
|
||||||
|
|
@ -73,13 +75,13 @@ class VideoClip extends BaseClip
|
||||||
$video = new Video([
|
$video = new Video([
|
||||||
'file_key' => $fileKey,
|
'file_key' => $fileKey,
|
||||||
'language_code' => $this->getPodcast()
|
'language_code' => $this->getPodcast()
|
||||||
->language_code,
|
->language_code,
|
||||||
'uploaded_by' => $this->attributes['created_by'],
|
'uploaded_by' => $this->attributes['created_by'],
|
||||||
'updated_by' => $this->attributes['created_by'],
|
'updated_by' => $this->attributes['created_by'],
|
||||||
]);
|
]);
|
||||||
$video->setFile($file);
|
$video->setFile($file);
|
||||||
|
|
||||||
$this->attributes['media_id'] = (new MediaModel('video'))->saveMedia($video);
|
$this->attributes['media_id'] = new MediaModel('video')->saveMedia($video);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,9 @@ class Credit extends Entity
|
||||||
|
|
||||||
public function getPerson(): ?Person
|
public function getPerson(): ?Person
|
||||||
{
|
{
|
||||||
if ($this->person_id === null) {
|
|
||||||
throw new RuntimeException('Credit must have person_id before getting person.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->person instanceof 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;
|
return $this->person;
|
||||||
|
|
@ -68,12 +65,9 @@ class Credit extends Entity
|
||||||
|
|
||||||
public function getPodcast(): ?Podcast
|
public function getPodcast(): ?Podcast
|
||||||
{
|
{
|
||||||
if ($this->podcast_id === null) {
|
|
||||||
throw new RuntimeException('Credit must have podcast_id before getting podcast.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->podcast instanceof 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;
|
return $this->podcast;
|
||||||
|
|
@ -86,7 +80,8 @@ class Credit extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->episode instanceof Episode) {
|
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;
|
return $this->episode;
|
||||||
|
|
@ -94,7 +89,7 @@ class Credit extends Entity
|
||||||
|
|
||||||
public function getGroupLabel(): string
|
public function getGroupLabel(): string
|
||||||
{
|
{
|
||||||
if ($this->person_group === null) {
|
if ($this->person_group === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
use App\Entities\Clip\Soundbite;
|
use App\Entities\Clip\Soundbite;
|
||||||
use App\Libraries\SimpleRSSElement;
|
|
||||||
use App\Models\ClipModel;
|
use App\Models\ClipModel;
|
||||||
use App\Models\EpisodeCommentModel;
|
use App\Models\EpisodeCommentModel;
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
|
|
@ -29,14 +28,12 @@ use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||||
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
|
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
|
||||||
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
|
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
|
||||||
use League\CommonMark\MarkdownConverter;
|
use League\CommonMark\MarkdownConverter;
|
||||||
use Modules\Analytics\OP3;
|
|
||||||
use Modules\Media\Entities\Audio;
|
use Modules\Media\Entities\Audio;
|
||||||
use Modules\Media\Entities\Chapters;
|
use Modules\Media\Entities\Chapters;
|
||||||
use Modules\Media\Entities\Image;
|
use Modules\Media\Entities\Image;
|
||||||
use Modules\Media\Entities\Transcript;
|
use Modules\Media\Entities\Transcript;
|
||||||
use Modules\Media\Models\MediaModel;
|
use Modules\Media\Models\MediaModel;
|
||||||
use RuntimeException;
|
use Override;
|
||||||
use SimpleXMLElement;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
|
|
@ -65,20 +62,18 @@ use SimpleXMLElement;
|
||||||
* @property Chapters|null $chapters
|
* @property Chapters|null $chapters
|
||||||
* @property string|null $chapters_remote_url
|
* @property string|null $chapters_remote_url
|
||||||
* @property string|null $parental_advisory
|
* @property string|null $parental_advisory
|
||||||
* @property int $number
|
* @property ?int $number
|
||||||
* @property int $season_number
|
* @property ?int $season_number
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property bool $is_blocked
|
* @property bool $is_blocked
|
||||||
* @property Location|null $location
|
* @property Location|null $location
|
||||||
* @property string|null $location_name
|
* @property string|null $location_name
|
||||||
* @property string|null $location_geo
|
* @property string|null $location_geo
|
||||||
* @property string|null $location_osm
|
* @property string|null $location_osm
|
||||||
* @property array|null $custom_rss
|
|
||||||
* @property string $custom_rss_string
|
|
||||||
* @property bool $is_published_on_hubs
|
* @property bool $is_published_on_hubs
|
||||||
|
* @property int $downloads_count
|
||||||
* @property int $posts_count
|
* @property int $posts_count
|
||||||
* @property int $comments_count
|
* @property int $comments_count
|
||||||
* @property int $downloads
|
|
||||||
* @property EpisodeComment[]|null $comments
|
* @property EpisodeComment[]|null $comments
|
||||||
* @property bool $is_premium
|
* @property bool $is_premium
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
|
|
@ -94,19 +89,19 @@ use SimpleXMLElement;
|
||||||
*/
|
*/
|
||||||
class Episode extends Entity
|
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 ?Audio $audio = null;
|
||||||
|
|
||||||
protected string $audio_url;
|
protected string $embed_url = '';
|
||||||
|
|
||||||
protected string $audio_web_url;
|
|
||||||
|
|
||||||
protected string $audio_opengraph_url;
|
|
||||||
|
|
||||||
protected string $embed_url;
|
|
||||||
|
|
||||||
protected ?Image $cover = null;
|
protected ?Image $cover = null;
|
||||||
|
|
||||||
|
|
@ -116,8 +111,6 @@ class Episode extends Entity
|
||||||
|
|
||||||
protected ?Chapters $chapters = null;
|
protected ?Chapters $chapters = null;
|
||||||
|
|
||||||
protected int $downloads = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Person[]|null
|
* @var Person[]|null
|
||||||
*/
|
*/
|
||||||
|
|
@ -140,8 +133,6 @@ class Episode extends Entity
|
||||||
|
|
||||||
protected ?Location $location = null;
|
protected ?Location $location = null;
|
||||||
|
|
||||||
protected string $custom_rss_string;
|
|
||||||
|
|
||||||
protected ?string $publication_status = null;
|
protected ?string $publication_status = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -176,8 +167,8 @@ class Episode extends Entity
|
||||||
'location_name' => '?string',
|
'location_name' => '?string',
|
||||||
'location_geo' => '?string',
|
'location_geo' => '?string',
|
||||||
'location_osm' => '?string',
|
'location_osm' => '?string',
|
||||||
'custom_rss' => '?json-array',
|
|
||||||
'is_published_on_hubs' => 'boolean',
|
'is_published_on_hubs' => 'boolean',
|
||||||
|
'downloads_count' => 'integer',
|
||||||
'posts_count' => 'integer',
|
'posts_count' => 'integer',
|
||||||
'comments_count' => 'integer',
|
'comments_count' => 'integer',
|
||||||
'is_premium' => 'boolean',
|
'is_premium' => 'boolean',
|
||||||
|
|
@ -185,7 +176,32 @@ class Episode extends Entity
|
||||||
'updated_by' => 'integer',
|
'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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -196,18 +212,19 @@ class Episode extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getCover()
|
$this->getCover()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('image'))->updateMedia($this->getCover());
|
new MediaModel('image')
|
||||||
|
->updateMedia($this->getCover());
|
||||||
} else {
|
} else {
|
||||||
$cover = new Image([
|
$cover = new Image([
|
||||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
|
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
|
||||||
'sizes' => config('Images')
|
'sizes' => config('Images')
|
||||||
->podcastCoverSizes,
|
->podcastCoverSizes,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$cover->setFile($file);
|
$cover->setFile($file);
|
||||||
|
|
||||||
$this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
|
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -226,12 +243,13 @@ class Episode extends Entity
|
||||||
return $this->cover;
|
return $this->cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
|
$this->cover = new MediaModel('image')
|
||||||
|
->getMediaById($this->cover_id);
|
||||||
|
|
||||||
return $this->cover;
|
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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -242,7 +260,8 @@ class Episode extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getAudio()
|
$this->getAudio()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('audio'))->updateMedia($this->getAudio());
|
new MediaModel('audio')
|
||||||
|
->updateMedia($this->getAudio());
|
||||||
} else {
|
} else {
|
||||||
$audio = new Audio([
|
$audio = new Audio([
|
||||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
|
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
|
||||||
|
|
@ -253,7 +272,7 @@ class Episode extends Entity
|
||||||
]);
|
]);
|
||||||
$audio->setFile($file);
|
$audio->setFile($file);
|
||||||
|
|
||||||
$this->attributes['audio_id'] = (new MediaModel())->saveMedia($audio);
|
$this->attributes['audio_id'] = new MediaModel()->saveMedia($audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -262,13 +281,14 @@ class Episode extends Entity
|
||||||
public function getAudio(): Audio
|
public function getAudio(): Audio
|
||||||
{
|
{
|
||||||
if (! $this->audio instanceof 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;
|
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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -279,18 +299,19 @@ class Episode extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getTranscript()
|
$this->getTranscript()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('transcript'))->updateMedia($this->getTranscript());
|
new MediaModel('transcript')
|
||||||
|
->updateMedia($this->getTranscript());
|
||||||
} else {
|
} else {
|
||||||
$transcript = new Transcript([
|
$transcript = new Transcript([
|
||||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
|
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
|
||||||
'language_code' => $this->getPodcast()
|
'language_code' => $this->getPodcast()
|
||||||
->language_code,
|
->language_code,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$transcript->setFile($file);
|
$transcript->setFile($file);
|
||||||
|
|
||||||
$this->attributes['transcript_id'] = (new MediaModel('transcript'))->saveMedia($transcript);
|
$this->attributes['transcript_id'] = new MediaModel('transcript')->saveMedia($transcript);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -299,13 +320,14 @@ class Episode extends Entity
|
||||||
public function getTranscript(): ?Transcript
|
public function getTranscript(): ?Transcript
|
||||||
{
|
{
|
||||||
if ($this->transcript_id !== null && ! $this->transcript instanceof 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;
|
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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -316,18 +338,19 @@ class Episode extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getChapters()
|
$this->getChapters()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('chapters'))->updateMedia($this->getChapters());
|
new MediaModel('chapters')
|
||||||
|
->updateMedia($this->getChapters());
|
||||||
} else {
|
} else {
|
||||||
$chapters = new Chapters([
|
$chapters = new Chapters([
|
||||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
|
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
|
||||||
'language_code' => $this->getPodcast()
|
'language_code' => $this->getPodcast()
|
||||||
->language_code,
|
->language_code,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$chapters->setFile($file);
|
$chapters->setFile($file);
|
||||||
|
|
||||||
$this->attributes['chapters_id'] = (new MediaModel('chapters'))->saveMedia($chapters);
|
$this->attributes['chapters_id'] = new MediaModel('chapters')->saveMedia($chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -336,46 +359,13 @@ class Episode extends Entity
|
||||||
public function getChapters(): ?Chapters
|
public function getChapters(): ?Chapters
|
||||||
{
|
{
|
||||||
if ($this->chapters_id !== null && ! $this->chapters instanceof 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;
|
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.
|
* Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null.
|
||||||
*/
|
*/
|
||||||
|
|
@ -407,12 +397,9 @@ class Episode extends Entity
|
||||||
*/
|
*/
|
||||||
public function getPersons(): array
|
public function getPersons(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Episode must be created before getting persons.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->persons === null) {
|
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;
|
return $this->persons;
|
||||||
|
|
@ -425,12 +412,9 @@ class Episode extends Entity
|
||||||
*/
|
*/
|
||||||
public function getSoundbites(): array
|
public function getSoundbites(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Episode must be created before getting soundbites.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->soundbites === null) {
|
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;
|
return $this->soundbites;
|
||||||
|
|
@ -441,12 +425,9 @@ class Episode extends Entity
|
||||||
*/
|
*/
|
||||||
public function getPosts(): array
|
public function getPosts(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Episode must be created before getting posts.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->posts === null) {
|
if ($this->posts === null) {
|
||||||
$this->posts = (new PostModel())->getEpisodePosts($this->id);
|
$this->posts = new PostModel()
|
||||||
|
->getEpisodePosts($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->posts;
|
return $this->posts;
|
||||||
|
|
@ -457,23 +438,15 @@ class Episode extends Entity
|
||||||
*/
|
*/
|
||||||
public function getComments(): array
|
public function getComments(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Episode must be created before getting comments.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->comments === null) {
|
if ($this->comments === null) {
|
||||||
$this->comments = (new EpisodeCommentModel())->getEpisodeComments($this->id);
|
$this->comments = new EpisodeCommentModel()
|
||||||
|
->getEpisodeComments($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->comments;
|
return $this->comments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLink(): string
|
public function getEmbedUrl(?string $theme = null): string
|
||||||
{
|
|
||||||
return url_to('episode', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEmbedUrl(string $theme = null): string
|
|
||||||
{
|
{
|
||||||
return $theme
|
return $theme
|
||||||
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
|
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
|
||||||
|
|
@ -482,14 +455,15 @@ class Episode extends Entity
|
||||||
|
|
||||||
public function setGuid(?string $guid = null): static
|
public function setGuid(?string $guid = null): static
|
||||||
{
|
{
|
||||||
$this->attributes['guid'] = $guid ?? $this->getLink();
|
$this->attributes['guid'] = $guid ?? $this->link;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPodcast(): ?Podcast
|
public function getPodcast(): ?Podcast
|
||||||
{
|
{
|
||||||
return (new PodcastModel())->getPodcastById($this->podcast_id);
|
return new PodcastModel()
|
||||||
|
->getPodcastById($this->podcast_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDescriptionMarkdown(string $descriptionMarkdown): static
|
public function setDescriptionMarkdown(string $descriptionMarkdown): static
|
||||||
|
|
@ -513,34 +487,6 @@ class Episode extends Entity
|
||||||
return $this;
|
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
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
if ($this->description === null) {
|
if ($this->description === null) {
|
||||||
|
|
@ -609,96 +555,11 @@ class Episode extends Entity
|
||||||
return $this->location;
|
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
|
public function getPreviewLink(): string
|
||||||
{
|
{
|
||||||
if ($this->preview_id === null) {
|
if ($this->preview_id === null) {
|
||||||
// generate preview id
|
// 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');
|
throw new Exception('Could not set episode preview id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -713,10 +574,7 @@ class Episode extends Entity
|
||||||
*/
|
*/
|
||||||
public function getClipCount(): int|string
|
public function getClipCount(): int|string
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
return new ClipModel()
|
||||||
throw new RuntimeException('Episode must be created before getting number of video clips.');
|
->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 Episode|null $episode
|
||||||
* @property int $actor_id
|
* @property int $actor_id
|
||||||
* @property Actor|null $actor
|
* @property Actor|null $actor
|
||||||
* @property string $in_reply_to_id
|
* @property ?string $in_reply_to_id
|
||||||
* @property EpisodeComment|null $reply_to_comment
|
* @property EpisodeComment|null $reply_to_comment
|
||||||
* @property string $message
|
* @property string $message
|
||||||
* @property string $message_html
|
* @property string $message_html
|
||||||
|
|
@ -75,12 +75,9 @@ class EpisodeComment extends UuidEntity
|
||||||
|
|
||||||
public function getEpisode(): ?Episode
|
public function getEpisode(): ?Episode
|
||||||
{
|
{
|
||||||
if ($this->episode_id === null) {
|
|
||||||
throw new RuntimeException('Comment must have an episode_id before getting episode.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->episode instanceof 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;
|
return $this->episode;
|
||||||
|
|
@ -91,10 +88,6 @@ class EpisodeComment extends UuidEntity
|
||||||
*/
|
*/
|
||||||
public function getActor(): ?Actor
|
public function getActor(): ?Actor
|
||||||
{
|
{
|
||||||
if ($this->actor_id === null) {
|
|
||||||
throw new RuntimeException('Comment must have an actor_id before getting actor.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->actor instanceof Actor) {
|
if (! $this->actor instanceof Actor) {
|
||||||
$this->actor = model(ActorModel::class, false)
|
$this->actor = model(ActorModel::class, false)
|
||||||
->getActorById($this->actor_id);
|
->getActorById($this->actor_id);
|
||||||
|
|
@ -108,12 +101,9 @@ class EpisodeComment extends UuidEntity
|
||||||
*/
|
*/
|
||||||
public function getReplies(): array
|
public function getReplies(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Comment must be created before getting replies.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->replies === null) {
|
if ($this->replies === null) {
|
||||||
$this->replies = (new EpisodeCommentModel())->getCommentReplies($this->id);
|
$this->replies = new EpisodeCommentModel()
|
||||||
|
->getCommentReplies($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->replies;
|
return $this->replies;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
use CodeIgniter\Entity\Entity;
|
use CodeIgniter\Entity\Entity;
|
||||||
use Config\Services;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $url
|
* @property string $url
|
||||||
|
|
@ -23,15 +22,9 @@ use Config\Services;
|
||||||
*/
|
*/
|
||||||
class Location extends Entity
|
class Location extends Entity
|
||||||
{
|
{
|
||||||
/**
|
private const string OSM_URL = 'https://www.openstreetmap.org/';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const OSM_URL = 'https://www.openstreetmap.org/';
|
|
||||||
|
|
||||||
/**
|
private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $name,
|
protected string $name,
|
||||||
|
|
@ -85,7 +78,7 @@ class Location extends Entity
|
||||||
*/
|
*/
|
||||||
public function fetchOsmLocation(): static
|
public function fetchOsmLocation(): static
|
||||||
{
|
{
|
||||||
$client = Services::curlrequest();
|
$client = service('curlrequest');
|
||||||
|
|
||||||
$response = $client->request(
|
$response = $client->request(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -109,14 +102,14 @@ class Location extends Entity
|
||||||
|
|
||||||
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
|
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
|
||||||
$places[0],
|
$places[0],
|
||||||
'lon'
|
'lon',
|
||||||
) && $places[0]->lon !== null)) {
|
) && $places[0]->lon !== null)) {
|
||||||
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
|
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
|
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
|
||||||
$places[0],
|
$places[0],
|
||||||
'osm_id'
|
'osm_id',
|
||||||
) && $places[0]->osm_id !== null)) {
|
) && $places[0]->osm_id !== null)) {
|
||||||
$this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
|
$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 $full_name
|
||||||
* @property string $unique_name
|
* @property string $unique_name
|
||||||
* @property string|null $information_url
|
* @property string|null $information_url
|
||||||
* @property int $avatar_id
|
* @property ?int $avatar_id
|
||||||
* @property ?Image $avatar
|
* @property ?Image $avatar
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
|
|
@ -56,7 +56,7 @@ class Person extends Entity
|
||||||
/**
|
/**
|
||||||
* Saves the person avatar in `public/media/persons/`
|
* 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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -67,18 +67,19 @@ class Person extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getAvatar()
|
$this->getAvatar()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('image'))->updateMedia($this->getAvatar());
|
new MediaModel('image')
|
||||||
|
->updateMedia($this->getAvatar());
|
||||||
} else {
|
} else {
|
||||||
$avatar = new Image([
|
$avatar = new Image([
|
||||||
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
|
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
|
||||||
'sizes' => config('Images')
|
'sizes' => config('Images')
|
||||||
->personAvatarSizes,
|
->personAvatarSizes,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$avatar->setFile($file);
|
$avatar->setFile($file);
|
||||||
|
|
||||||
$this->attributes['avatar_id'] = (new MediaModel('image'))->saveMedia($avatar);
|
$this->attributes['avatar_id'] = new MediaModel('image')->saveMedia($avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -91,7 +92,8 @@ class Person extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->avatar instanceof Image) {
|
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;
|
return $this->avatar;
|
||||||
|
|
@ -107,11 +109,12 @@ class Person extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->roles === null) {
|
if ($this->roles === null) {
|
||||||
$this->roles = (new PersonModel())->getPersonRoles(
|
$this->roles = new PersonModel()
|
||||||
$this->id,
|
->getPersonRoles(
|
||||||
(int) $this->attributes['podcast_id'],
|
$this->id,
|
||||||
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null
|
(int) $this->attributes['podcast_id'],
|
||||||
);
|
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->roles;
|
return $this->roles;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
use App\Libraries\SimpleRSSElement;
|
|
||||||
use App\Models\ActorModel;
|
use App\Models\ActorModel;
|
||||||
use App\Models\CategoryModel;
|
use App\Models\CategoryModel;
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
|
|
@ -62,12 +61,8 @@ use RuntimeException;
|
||||||
* @property string|null $publisher
|
* @property string|null $publisher
|
||||||
* @property string $owner_name
|
* @property string $owner_name
|
||||||
* @property string $owner_email
|
* @property string $owner_email
|
||||||
* @property bool $is_owner_email_removed_from_feed
|
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $medium
|
|
||||||
* @property string|null $copyright
|
* @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_blocked
|
||||||
* @property bool $is_completed
|
* @property bool $is_completed
|
||||||
* @property bool $is_locked
|
* @property bool $is_locked
|
||||||
|
|
@ -77,15 +72,7 @@ use RuntimeException;
|
||||||
* @property string|null $location_name
|
* @property string|null $location_name
|
||||||
* @property string|null $location_geo
|
* @property string|null $location_geo
|
||||||
* @property string|null $location_osm
|
* @property string|null $location_osm
|
||||||
* @property string|null $payment_pointer
|
|
||||||
* @property array|null $custom_rss
|
|
||||||
* @property bool $is_op3_enabled
|
|
||||||
* @property string $op3_url
|
|
||||||
* @property string $custom_rss_string
|
|
||||||
* @property bool $is_published_on_hubs
|
* @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 $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
* @property string $publication_status
|
* @property string $publication_status
|
||||||
|
|
@ -125,9 +112,9 @@ class Podcast extends Entity
|
||||||
protected ?array $other_categories = null;
|
protected ?array $other_categories = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string[]|null
|
* @var int[]
|
||||||
*/
|
*/
|
||||||
protected ?array $other_categories_ids = null;
|
protected array $other_categories_ids = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Episode[]|null
|
* @var Episode[]|null
|
||||||
|
|
@ -166,8 +153,6 @@ class Podcast extends Entity
|
||||||
|
|
||||||
protected ?Location $location = null;
|
protected ?Location $location = null;
|
||||||
|
|
||||||
protected string $custom_rss_string;
|
|
||||||
|
|
||||||
protected ?string $publication_status = null;
|
protected ?string $publication_status = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -180,44 +165,35 @@ class Podcast extends Entity
|
||||||
* @var array<string, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'id' => 'integer',
|
'id' => 'integer',
|
||||||
'guid' => 'string',
|
'guid' => 'string',
|
||||||
'actor_id' => 'integer',
|
'actor_id' => 'integer',
|
||||||
'handle' => 'string',
|
'handle' => 'string',
|
||||||
'title' => 'string',
|
'title' => 'string',
|
||||||
'description_markdown' => 'string',
|
'description_markdown' => 'string',
|
||||||
'description_html' => 'string',
|
'description_html' => 'string',
|
||||||
'cover_id' => 'int',
|
'cover_id' => 'int',
|
||||||
'banner_id' => '?int',
|
'banner_id' => '?int',
|
||||||
'language_code' => 'string',
|
'language_code' => 'string',
|
||||||
'category_id' => 'integer',
|
'category_id' => 'integer',
|
||||||
'parental_advisory' => '?string',
|
'parental_advisory' => '?string',
|
||||||
'publisher' => '?string',
|
'publisher' => '?string',
|
||||||
'owner_name' => 'string',
|
'owner_name' => 'string',
|
||||||
'owner_email' => 'string',
|
'owner_email' => 'string',
|
||||||
'is_owner_email_removed_from_feed' => 'boolean',
|
'type' => 'string',
|
||||||
'type' => 'string',
|
'copyright' => '?string',
|
||||||
'medium' => 'string',
|
'is_blocked' => 'boolean',
|
||||||
'copyright' => '?string',
|
'is_completed' => 'boolean',
|
||||||
'episode_description_footer_markdown' => '?string',
|
'is_locked' => 'boolean',
|
||||||
'episode_description_footer_html' => '?string',
|
'is_premium_by_default' => 'boolean',
|
||||||
'is_blocked' => 'boolean',
|
'imported_feed_url' => '?string',
|
||||||
'is_completed' => 'boolean',
|
'new_feed_url' => '?string',
|
||||||
'is_locked' => 'boolean',
|
'location_name' => '?string',
|
||||||
'is_premium_by_default' => 'boolean',
|
'location_geo' => '?string',
|
||||||
'imported_feed_url' => '?string',
|
'location_osm' => '?string',
|
||||||
'new_feed_url' => '?string',
|
'is_published_on_hubs' => 'boolean',
|
||||||
'location_name' => '?string',
|
'created_by' => 'integer',
|
||||||
'location_geo' => '?string',
|
'updated_by' => 'integer',
|
||||||
'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',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getAtHandle(): string
|
public function getAtHandle(): string
|
||||||
|
|
@ -239,7 +215,7 @@ class Podcast extends Entity
|
||||||
return $this->actor;
|
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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -250,18 +226,19 @@ class Podcast extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getCover()
|
$this->getCover()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('image'))->updateMedia($this->getCover());
|
new MediaModel('image')
|
||||||
|
->updateMedia($this->getCover());
|
||||||
} else {
|
} else {
|
||||||
$cover = new Image([
|
$cover = new Image([
|
||||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
|
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
|
||||||
'sizes' => config('Images')
|
'sizes' => config('Images')
|
||||||
->podcastCoverSizes,
|
->podcastCoverSizes,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$cover->setFile($file);
|
$cover->setFile($file);
|
||||||
|
|
||||||
$this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
|
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -270,7 +247,8 @@ class Podcast extends Entity
|
||||||
public function getCover(): Image
|
public function getCover(): Image
|
||||||
{
|
{
|
||||||
if (! $this->cover instanceof 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) {
|
if (! $cover instanceof Image) {
|
||||||
throw new Exception('Could not retrieve podcast cover.');
|
throw new Exception('Could not retrieve podcast cover.');
|
||||||
|
|
@ -282,7 +260,7 @@ class Podcast extends Entity
|
||||||
return $this->cover;
|
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())) {
|
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -293,18 +271,19 @@ class Podcast extends Entity
|
||||||
->setFile($file);
|
->setFile($file);
|
||||||
$this->getBanner()
|
$this->getBanner()
|
||||||
->updated_by = $this->attributes['updated_by'];
|
->updated_by = $this->attributes['updated_by'];
|
||||||
(new MediaModel('image'))->updateMedia($this->getBanner());
|
new MediaModel('image')
|
||||||
|
->updateMedia($this->getBanner());
|
||||||
} else {
|
} else {
|
||||||
$banner = new Image([
|
$banner = new Image([
|
||||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
|
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
|
||||||
'sizes' => config('Images')
|
'sizes' => config('Images')
|
||||||
->podcastBannerSizes,
|
->podcastBannerSizes,
|
||||||
'uploaded_by' => $this->attributes['updated_by'],
|
'uploaded_by' => $this->attributes['updated_by'],
|
||||||
'updated_by' => $this->attributes['updated_by'],
|
'updated_by' => $this->attributes['updated_by'],
|
||||||
]);
|
]);
|
||||||
$banner->setFile($file);
|
$banner->setFile($file);
|
||||||
|
|
||||||
$this->attributes['banner_id'] = (new MediaModel('image'))->saveMedia($banner);
|
$this->attributes['banner_id'] = new MediaModel('image')->saveMedia($banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -317,7 +296,8 @@ class Podcast extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->banner instanceof Image) {
|
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;
|
return $this->banner;
|
||||||
|
|
@ -340,12 +320,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getEpisodes(): array
|
public function getEpisodes(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting episodes.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->episodes === null) {
|
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;
|
return $this->episodes;
|
||||||
|
|
@ -356,11 +333,8 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getEpisodesCount(): int|string
|
public function getEpisodesCount(): int|string
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
return new EpisodeModel()
|
||||||
throw new RuntimeException('Podcast must be created before getting number of episodes.');
|
->getPodcastEpisodesCount($this->id);
|
||||||
}
|
|
||||||
|
|
||||||
return (new EpisodeModel())->getPodcastEpisodesCount($this->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -370,12 +344,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getPersons(): array
|
public function getPersons(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting persons.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->persons === null) {
|
if ($this->persons === null) {
|
||||||
$this->persons = (new PersonModel())->getPodcastPersons($this->id);
|
$this->persons = new PersonModel()
|
||||||
|
->getPodcastPersons($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->persons;
|
return $this->persons;
|
||||||
|
|
@ -386,12 +357,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getCategory(): ?Category
|
public function getCategory(): ?Category
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting category.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->category instanceof 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;
|
return $this->category;
|
||||||
|
|
@ -404,12 +372,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getSubscriptions(): array
|
public function getSubscriptions(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcasts must be created before getting subscriptions.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->subscriptions === null) {
|
if ($this->subscriptions === null) {
|
||||||
$this->subscriptions = (new SubscriptionModel())->getPodcastSubscriptions($this->id);
|
$this->subscriptions = new SubscriptionModel()
|
||||||
|
->getPodcastSubscriptions($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->subscriptions;
|
return $this->subscriptions;
|
||||||
|
|
@ -422,12 +387,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getContributors(): array
|
public function getContributors(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcasts must be created before getting contributors.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->contributors === null) {
|
if ($this->contributors === null) {
|
||||||
$this->contributors = (new UserModel())->getPodcastContributors($this->id);
|
$this->contributors = new UserModel()
|
||||||
|
->getPodcastContributors($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->contributors;
|
return $this->contributors;
|
||||||
|
|
@ -454,42 +416,6 @@ class Podcast extends Entity
|
||||||
return $this;
|
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
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
if ($this->description === null) {
|
if ($this->description === null) {
|
||||||
|
|
@ -523,12 +449,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getPodcastingPlatforms(): array
|
public function getPodcastingPlatforms(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting podcasting platform links.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->podcasting_platforms === null) {
|
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;
|
return $this->podcasting_platforms;
|
||||||
|
|
@ -541,12 +464,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getSocialPlatforms(): array
|
public function getSocialPlatforms(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting social platform links.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->social_platforms === null) {
|
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;
|
return $this->social_platforms;
|
||||||
|
|
@ -559,12 +479,9 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getFundingPlatforms(): array
|
public function getFundingPlatforms(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting funding platform links.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->funding_platforms === null) {
|
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;
|
return $this->funding_platforms;
|
||||||
|
|
@ -575,23 +492,20 @@ class Podcast extends Entity
|
||||||
*/
|
*/
|
||||||
public function getOtherCategories(): array
|
public function getOtherCategories(): array
|
||||||
{
|
{
|
||||||
if ($this->id === null) {
|
|
||||||
throw new RuntimeException('Podcast must be created before getting other categories.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->other_categories === null) {
|
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 $this->other_categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int[]|string[]
|
* @return int[]
|
||||||
*/
|
*/
|
||||||
public function getOtherCategoriesIds(): array
|
public function getOtherCategoriesIds(): array
|
||||||
{
|
{
|
||||||
if ($this->other_categories_ids === null) {
|
if ($this->other_categories_ids === []) {
|
||||||
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
|
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -638,68 +552,10 @@ class Podcast extends Entity
|
||||||
return $this->location;
|
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
|
public function getIsPremium(): bool
|
||||||
{
|
{
|
||||||
// podcast is premium if at least one of its episodes is set as premium
|
// podcast is premium if at least one of its episodes is set as premium
|
||||||
return (new EpisodeModel())->doesPodcastHavePremiumEpisodes($this->id);
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class Post extends FediversePost
|
||||||
'episode_id' => '?integer',
|
'episode_id' => '?integer',
|
||||||
'message' => 'string',
|
'message' => 'string',
|
||||||
'message_html' => 'string',
|
'message_html' => 'string',
|
||||||
|
'is_private' => 'boolean',
|
||||||
'favourites_count' => 'integer',
|
'favourites_count' => 'integer',
|
||||||
'reblogs_count' => 'integer',
|
'reblogs_count' => 'integer',
|
||||||
'replies_count' => 'integer',
|
'replies_count' => 'integer',
|
||||||
|
|
@ -50,7 +51,8 @@ class Post extends FediversePost
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->episode instanceof 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;
|
return $this->episode;
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,28 @@ namespace App\Filters;
|
||||||
use CodeIgniter\Filters\FilterInterface;
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class AllowCorsFilter implements FilterInterface
|
class AllowCorsFilter implements FilterInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param string[]|null $arguments
|
* @param list<string>|null $arguments
|
||||||
|
*
|
||||||
|
* @return RequestInterface|ResponseInterface|string|null
|
||||||
*/
|
*/
|
||||||
public function before(RequestInterface $request, $arguments = null): void
|
#[Override]
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
{
|
{
|
||||||
// Do something here
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[]|null $arguments
|
* @param list<string>|null $arguments
|
||||||
|
*
|
||||||
|
* @return ResponseInterface|null
|
||||||
*/
|
*/
|
||||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
|
#[Override]
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
{
|
{
|
||||||
if (! $response->hasHeader('Cache-Control')) {
|
if (! $response->hasHeader('Cache-Control')) {
|
||||||
$response->setHeader('Cache-Control', 'public, max-age=86400');
|
$response->setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
|
@ -31,5 +38,7 @@ class AllowCorsFilter implements FilterInterface
|
||||||
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, 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-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
|
||||||
->setHeader('Access-Control-Max-Age', '86400');
|
->setHeader('Access-Control-Max-Age', '86400');
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
|
||||||
* @copyright 2020 Ad Aures
|
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
|
||||||
* @link https://castopod.org/
|
|
||||||
*/
|
|
||||||
|
|
||||||
use Config\Services;
|
|
||||||
|
|
||||||
if (! function_exists('render_breadcrumb')) {
|
if (! function_exists('render_breadcrumb')) {
|
||||||
/**
|
/**
|
||||||
* Renders the breadcrumb navigation through the Breadcrumb service
|
* Renders the breadcrumb navigation through the Breadcrumb service
|
||||||
|
|
@ -17,20 +9,18 @@ if (! function_exists('render_breadcrumb')) {
|
||||||
* @param string|null $class to be added to the breadcrumb nav
|
* @param string|null $class to be added to the breadcrumb nav
|
||||||
* @return string html breadcrumb
|
* @return string html breadcrumb
|
||||||
*/
|
*/
|
||||||
function render_breadcrumb(string $class = null): string
|
function render_breadcrumb(?string $class = null): string
|
||||||
{
|
{
|
||||||
$breadcrumb = Services::breadcrumb();
|
return service('breadcrumb')->render($class);
|
||||||
return $breadcrumb->render($class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('replace_breadcrumb_params')) {
|
if (! function_exists('replace_breadcrumb_params')) {
|
||||||
/**
|
/**
|
||||||
* @param string[] $newParams
|
* @param array<string|int,string> $newParams
|
||||||
*/
|
*/
|
||||||
function replace_breadcrumb_params(array $newParams): void
|
function replace_breadcrumb_params(array $newParams): void
|
||||||
{
|
{
|
||||||
$breadcrumb = Services::breadcrumb();
|
service('breadcrumb')->replaceParams($newParams);
|
||||||
$breadcrumb->replaceParams(esc($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')) {
|
if (! function_exists('data_table')) {
|
||||||
/**
|
/**
|
||||||
* Data table component
|
* Data table component
|
||||||
|
|
@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) {
|
||||||
*/
|
*/
|
||||||
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
||||||
{
|
{
|
||||||
$class = match ($publicationStatus) {
|
$variant = match ($publicationStatus) {
|
||||||
'published' => 'text-pine-500 border-pine-500 bg-pine-50',
|
'published' => 'success',
|
||||||
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
|
'scheduled' => 'warning',
|
||||||
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50',
|
'with_podcast' => 'info',
|
||||||
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
|
'not_published' => 'default',
|
||||||
default => 'text-gray-600 border-gray-600 bg-gray-50',
|
default => 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
$title = match ($publicationStatus) {
|
$title = match ($publicationStatus) {
|
||||||
|
|
@ -130,16 +105,12 @@ if (! function_exists('publication_pill')) {
|
||||||
|
|
||||||
$label = lang('Episode.publication_status.' . $publicationStatus);
|
$label = lang('Episode.publication_status.' . $publicationStatus);
|
||||||
|
|
||||||
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' .
|
// @icon("error-warning-fill")
|
||||||
$class .
|
return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
|
||||||
' ' .
|
'">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||||
$customClass .
|
|
||||||
'">' .
|
|
||||||
$label .
|
|
||||||
($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
|
||||||
'class' => 'flex-shrink-0 ml-1 text-lg',
|
'class' => 'flex-shrink-0 ml-1 text-lg',
|
||||||
]) : '') .
|
]) : '') .
|
||||||
'</span>';
|
'</x-Pill>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,20 +129,20 @@ if (! function_exists('publication_button')) {
|
||||||
$label = lang('Episode.publish');
|
$label = lang('Episode.publish');
|
||||||
$route = route_to('episode-publish', $podcastId, $episodeId);
|
$route = route_to('episode-publish', $podcastId, $episodeId);
|
||||||
$variant = 'primary';
|
$variant = 'primary';
|
||||||
$iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
|
$iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
|
||||||
break;
|
break;
|
||||||
case 'with_podcast':
|
case 'with_podcast':
|
||||||
case 'scheduled':
|
case 'scheduled':
|
||||||
$label = lang('Episode.publish_edit');
|
$label = lang('Episode.publish_edit');
|
||||||
$route = route_to('episode-publish_edit', $podcastId, $episodeId);
|
$route = route_to('episode-publish_edit', $podcastId, $episodeId);
|
||||||
$variant = 'warning';
|
$variant = 'warning';
|
||||||
$iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
|
$iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
|
||||||
break;
|
break;
|
||||||
case 'published':
|
case 'published':
|
||||||
$label = lang('Episode.unpublish');
|
$label = lang('Episode.unpublish');
|
||||||
$route = route_to('episode-unpublish', $podcastId, $episodeId);
|
$route = route_to('episode-unpublish', $podcastId, $episodeId);
|
||||||
$variant = 'danger';
|
$variant = 'danger';
|
||||||
$iconLeft = 'cloud-off-fill'; // @icon('cloud-off-fill')
|
$iconLeft = 'cloud-off-fill'; // @icon("cloud-off-fill")
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$label = '';
|
$label = '';
|
||||||
|
|
@ -182,7 +153,7 @@ if (! function_exists('publication_button')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button>
|
<x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -263,7 +234,7 @@ if (! function_exists('episode_publication_status_banner')) {
|
||||||
$bannerText = lang('Episode.publication_status_banner.text', [
|
$bannerText = lang('Episode.publication_status_banner.text', [
|
||||||
'publication_status' => $episode->publication_status,
|
'publication_status' => $episode->publication_status,
|
||||||
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
|
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
|
||||||
$episode->published_at
|
$episode->published_at,
|
||||||
) : null,
|
) : null,
|
||||||
]);
|
]);
|
||||||
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
|
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
|
||||||
|
|
@ -296,7 +267,7 @@ if (! function_exists('episode_numbering')) {
|
||||||
?int $episodeNumber = null,
|
?int $episodeNumber = null,
|
||||||
?int $seasonNumber = null,
|
?int $seasonNumber = null,
|
||||||
string $class = '',
|
string $class = '',
|
||||||
bool $isAbbr = false
|
bool $isAbbr = false,
|
||||||
): string {
|
): string {
|
||||||
if (! $episodeNumber && ! $seasonNumber) {
|
if (! $episodeNumber && ! $seasonNumber) {
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -356,7 +327,7 @@ if (! function_exists('location_link')) {
|
||||||
'class' => 'mr-2 flex-shrink-0',
|
'class' => 'mr-2 flex-shrink-0',
|
||||||
]) . '<span class="truncate">' . esc($location->name) . '</span>',
|
]) . '<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}"),
|
($class === '' ? '' : " {$class}"),
|
||||||
'target' => '_blank',
|
'target' => '_blank',
|
||||||
'rel' => 'noreferrer noopener',
|
'rel' => 'noreferrer noopener',
|
||||||
|
|
@ -381,15 +352,15 @@ if (! function_exists('audio_player')) {
|
||||||
id="castopod-vm-player"
|
id="castopod-vm-player"
|
||||||
theme="light"
|
theme="light"
|
||||||
language="{$language}"
|
language="{$language}"
|
||||||
icons="castopod-icons"
|
|
||||||
class="{$class} relative z-0"
|
class="{$class} relative z-0"
|
||||||
|
icons="castopod-vm-player-icons"
|
||||||
style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));"
|
style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));"
|
||||||
>
|
>
|
||||||
<vm-audio preload="none">
|
<vm-audio preload="none">
|
||||||
<source src="{$source}" type="{$mediaType}" />
|
<source src="{$source}" type="{$mediaType}" />
|
||||||
</vm-audio>
|
</vm-audio>
|
||||||
<vm-ui>
|
<vm-ui>
|
||||||
<vm-icon-library name="castopod-icons"></vm-icon-library>
|
<vm-icon-library name="castopod-vm-player-icons"></vm-icon-library>
|
||||||
<vm-controls full-width>
|
<vm-controls full-width>
|
||||||
<vm-playback-control></vm-playback-control>
|
<vm-playback-control></vm-playback-control>
|
||||||
<vm-volume-control></vm-volume-control>
|
<vm-volume-control></vm-volume-control>
|
||||||
|
|
@ -411,7 +382,7 @@ if (! function_exists('relative_time')) {
|
||||||
function relative_time(Time $time, string $class = ''): string
|
function relative_time(Time $time, string $class = ''): string
|
||||||
{
|
{
|
||||||
$formatter = new IntlDateFormatter(service(
|
$formatter = new IntlDateFormatter(service(
|
||||||
'request'
|
'request',
|
||||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
||||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||||
$datetime = $time->format(DateTime::ATOM);
|
$datetime = $time->format(DateTime::ATOM);
|
||||||
|
|
@ -432,7 +403,7 @@ if (! function_exists('local_datetime')) {
|
||||||
function local_datetime(Time $time): string
|
function local_datetime(Time $time): string
|
||||||
{
|
{
|
||||||
$formatter = new IntlDateFormatter(service(
|
$formatter = new IntlDateFormatter(service(
|
||||||
'request'
|
'request',
|
||||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
||||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||||
$datetime = $time->format(DateTime::ATOM);
|
$datetime = $time->format(DateTime::ATOM);
|
||||||
|
|
@ -461,7 +432,7 @@ if (! function_exists('local_date')) {
|
||||||
function local_date(Time $time): string
|
function local_date(Time $time): string
|
||||||
{
|
{
|
||||||
$formatter = new IntlDateFormatter(service(
|
$formatter = new IntlDateFormatter(service(
|
||||||
'request'
|
'request',
|
||||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
|
||||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
$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.
|
// 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(
|
if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos(
|
||||||
(string) preg_replace('~\s+~', '', $extra),
|
(string) preg_replace('~\s+~', '', $extra),
|
||||||
'rows='
|
'rows=',
|
||||||
) !== false)) {
|
) !== false)) {
|
||||||
unset($defaults['rows']);
|
unset($defaults['rows']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
|
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
|
||||||
(string) preg_replace('~\s+~', '', $extra),
|
(string) preg_replace('~\s+~', '', $extra),
|
||||||
'cols='
|
'cols=',
|
||||||
) !== false)) {
|
) !== false)) {
|
||||||
unset($defaults['cols']);
|
unset($defaults['cols']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes(
|
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes(
|
||||||
$extra
|
$extra,
|
||||||
) . '>' . $val . "</textarea>\n";
|
) . '>' . $val . "</textarea>\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,18 +206,18 @@ if (! function_exists('get_podcast_banner')) {
|
||||||
if (! $podcast->banner instanceof Image) {
|
if (! $podcast->banner instanceof Image) {
|
||||||
$defaultBanner = config('Images')
|
$defaultBanner = config('Images')
|
||||||
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
|
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
|
||||||
Images::class
|
Images::class,
|
||||||
)->podcastBannerDefaultPaths['default'];
|
)->podcastBannerDefaultPaths['default'];
|
||||||
|
|
||||||
$sizes = config('Images')
|
$sizes = config('Images')
|
||||||
->podcastBannerSizes;
|
->podcastBannerSizes;
|
||||||
|
|
||||||
$sizeConfig = $sizes[$size];
|
$sizeConfig = $sizes[$size];
|
||||||
helper('filesystem');
|
helper('filesystem');
|
||||||
|
|
||||||
// return default site icon url
|
// return default site icon url
|
||||||
return base_url(
|
return base_url(
|
||||||
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null)
|
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,14 +231,14 @@ if (! function_exists('get_podcast_banner_mimetype')) {
|
||||||
{
|
{
|
||||||
if (! $podcast->banner instanceof Image) {
|
if (! $podcast->banner instanceof Image) {
|
||||||
$sizes = config('Images')
|
$sizes = config('Images')
|
||||||
->podcastBannerSizes;
|
->podcastBannerSizes;
|
||||||
|
|
||||||
$sizeConfig = $sizes[$size];
|
$sizeConfig = $sizes[$size];
|
||||||
helper('filesystem');
|
helper('filesystem');
|
||||||
|
|
||||||
// return default site icon url
|
// return default site icon url
|
||||||
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
|
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
|
||||||
Images::class
|
Images::class,
|
||||||
)->podcastBannerDefaultMimeType;
|
)->podcastBannerDefaultMimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,10 +252,10 @@ if (! function_exists('get_avatar_url')) {
|
||||||
{
|
{
|
||||||
if (! $person->avatar instanceof Image) {
|
if (! $person->avatar instanceof Image) {
|
||||||
$defaultAvatarPath = config('Images')
|
$defaultAvatarPath = config('Images')
|
||||||
->avatarDefaultPath;
|
->avatarDefaultPath;
|
||||||
|
|
||||||
$sizes = config('Images')
|
$sizes = config('Images')
|
||||||
->personAvatarSizes;
|
->personAvatarSizes;
|
||||||
|
|
||||||
$sizeConfig = $sizes[$size];
|
$sizeConfig = $sizes[$size];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,34 +16,35 @@ if (! function_exists('render_page_links')) {
|
||||||
*
|
*
|
||||||
* @return string html pages navigation
|
* @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'), [
|
$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) {
|
if ($podcastHandle !== null) {
|
||||||
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
|
$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'), [
|
$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'), [
|
$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) {
|
foreach ($pages as $page) {
|
||||||
$links .= anchor($page->link, esc($page->title), [
|
$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 set in .env, add legal notice link at the end of page links
|
||||||
if (config('App')->legalNoticeURL !== null) {
|
if (config('App')->legalNoticeURL !== null) {
|
||||||
$links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
|
$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',
|
'target' => '_blank',
|
||||||
'rel' => 'noopener noreferrer',
|
'rel' => 'noopener noreferrer',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ declare(strict_types=1);
|
||||||
use App\Entities\Category;
|
use App\Entities\Category;
|
||||||
use App\Entities\Location;
|
use App\Entities\Location;
|
||||||
use App\Entities\Podcast;
|
use App\Entities\Podcast;
|
||||||
use App\Libraries\SimpleRSSElement;
|
use App\Libraries\RssFeed;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\I18n\Time;
|
use CodeIgniter\I18n\Time;
|
||||||
use Config\Mimes;
|
use Config\Mimes;
|
||||||
use Modules\Media\Entities\Chapters;
|
use Modules\Media\Entities\Chapters;
|
||||||
use Modules\Media\Entities\Transcript;
|
use Modules\Media\Entities\Transcript;
|
||||||
|
use Modules\Plugins\Core\Plugins;
|
||||||
use Modules\PremiumPodcasts\Entities\Subscription;
|
use Modules\PremiumPodcasts\Entities\Subscription;
|
||||||
|
|
||||||
if (! function_exists('get_rss_feed')) {
|
if (! function_exists('get_rss_feed')) {
|
||||||
|
|
@ -28,24 +29,21 @@ if (! function_exists('get_rss_feed')) {
|
||||||
function get_rss_feed(
|
function get_rss_feed(
|
||||||
Podcast $podcast,
|
Podcast $podcast,
|
||||||
string $serviceSlug = '',
|
string $serviceSlug = '',
|
||||||
Subscription $subscription = null,
|
?Subscription $subscription = null,
|
||||||
string $token = null
|
?string $token = null,
|
||||||
): string {
|
): string {
|
||||||
|
/** @var Plugins $plugins */
|
||||||
|
$plugins = service('plugins');
|
||||||
|
|
||||||
$episodes = $podcast->episodes;
|
$episodes = $podcast->episodes;
|
||||||
|
|
||||||
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
$rss = new RssFeed();
|
||||||
|
|
||||||
$podcastNamespace = 'https://podcastindex.org/namespace/1.0';
|
$plugins->rssBeforeChannel($podcast);
|
||||||
|
|
||||||
$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>"
|
|
||||||
);
|
|
||||||
|
|
||||||
$channel = $rss->addChild('channel');
|
$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('href', $podcast->feed_url);
|
||||||
$atomLink->addAttribute('rel', 'self');
|
$atomLink->addAttribute('rel', 'self');
|
||||||
$atomLink->addAttribute('type', 'application/rss+xml');
|
$atomLink->addAttribute('type', 'application/rss+xml');
|
||||||
|
|
@ -54,18 +52,18 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$websubHubs = config('WebSub')
|
$websubHubs = config('WebSub')
|
||||||
->hubs;
|
->hubs;
|
||||||
foreach ($websubHubs as $websubHub) {
|
foreach ($websubHubs as $websubHub) {
|
||||||
$atomLinkHub = $channel->addChild('link', null, $atomNamespace);
|
$atomLinkHub = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
|
||||||
$atomLinkHub->addAttribute('href', $websubHub);
|
$atomLinkHub->addAttribute('href', $websubHub);
|
||||||
$atomLinkHub->addAttribute('rel', 'hub');
|
$atomLinkHub->addAttribute('rel', 'hub');
|
||||||
$atomLinkHub->addAttribute('type', 'application/rss+xml');
|
$atomLinkHub->addAttribute('type', 'application/rss+xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($podcast->new_feed_url !== null) {
|
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
|
// 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('generator', 'Castopod - https://castopod.org/');
|
||||||
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
|
$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)
|
$podcast->guid = $uuid->uuid5('ead4c236-bf58-58c6-a2c6-a6b28d128cb6', $podcast->feed_url)
|
||||||
->toString();
|
->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->addChild('title', $podcast->title, null, false);
|
||||||
$channel->addChildWithCDATA('description', $podcast->description_html);
|
$channel->addChildWithCDATA('description', $podcast->description_html);
|
||||||
|
|
||||||
$channel->addChild('medium', $podcast->medium, $podcastNamespace);
|
$itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
|
||||||
|
|
||||||
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
|
|
||||||
|
|
||||||
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
|
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
|
||||||
|
|
||||||
$channel->addChild('language', $podcast->language_code);
|
$channel->addChild('language', $podcast->language_code);
|
||||||
if ($podcast->location instanceof Location) {
|
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) {
|
if ($podcast->location->geo !== null) {
|
||||||
$locationElement->addAttribute('geo', $podcast->location->geo);
|
$locationElement->addAttribute('geo', $podcast->location->geo);
|
||||||
}
|
}
|
||||||
|
|
@ -101,49 +98,25 @@ if (! function_exists('get_rss_feed')) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($podcast->payment_pointer !== null) {
|
$channel
|
||||||
$valueElement = $channel->addChild('value', null, $podcastNamespace);
|
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
|
||||||
$valueElement->addAttribute('type', 'webmonetization');
|
->addAttribute('owner', $podcast->owner_email);
|
||||||
$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');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($podcast->imported_feed_url !== null) {
|
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) {
|
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);
|
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);
|
||||||
if ($podcastingPlatform->account_id !== null) {
|
if ($podcastingPlatform->account_id !== null) {
|
||||||
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);
|
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($podcastingPlatform->link_url !== null) {
|
$podcastingPlatformElement->addAttribute('url', $podcastingPlatform->link_url);
|
||||||
$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('priority', '1');
|
||||||
$castopodSocialElement->addAttribute('platform', 'castopod');
|
$castopodSocialElement->addAttribute('platform', 'castopod');
|
||||||
$castopodSocialElement->addAttribute('protocol', 'activitypub');
|
$castopodSocialElement->addAttribute('protocol', 'activitypub');
|
||||||
|
|
@ -151,7 +124,7 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
|
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
|
||||||
|
|
||||||
foreach ($podcast->social_platforms as $socialPlatform) {
|
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('priority', '2');
|
||||||
$socialElement->addAttribute('platform', $socialPlatform->slug);
|
$socialElement->addAttribute('platform', $socialPlatform->slug);
|
||||||
|
|
||||||
|
|
@ -159,7 +132,7 @@ if (! function_exists('get_rss_feed')) {
|
||||||
if (in_array(
|
if (in_array(
|
||||||
$socialPlatform->slug,
|
$socialPlatform->slug,
|
||||||
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
|
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
|
||||||
true
|
true,
|
||||||
)) {
|
)) {
|
||||||
$socialElement->addAttribute('protocol', 'activitypub');
|
$socialElement->addAttribute('protocol', 'activitypub');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,46 +143,44 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$socialElement->addAttribute('accountId', esc($socialPlatform->account_id));
|
$socialElement->addAttribute('accountId', esc($socialPlatform->account_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($socialPlatform->link_url !== null) {
|
$socialElement->addAttribute('accountUrl', esc($socialPlatform->link_url));
|
||||||
$socialElement->addAttribute('accountUrl', esc($socialPlatform->link_url));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($socialPlatform->slug === 'mastodon') {
|
if ($socialPlatform->slug === 'mastodon') {
|
||||||
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, $podcastNamespace);
|
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, RssFeed::PODCAST_NAMESPACE);
|
||||||
$socialSignUpelement->addAttribute('priority', '1');
|
$socialSignUpelement->addAttribute('priority', '1');
|
||||||
$socialSignUpelement->addAttribute(
|
$socialSignUpelement->addAttribute(
|
||||||
'homeUrl',
|
'homeUrl',
|
||||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||||
(string) $socialPlatform->link_url,
|
(string) $socialPlatform->link_url,
|
||||||
PHP_URL_HOST
|
PHP_URL_HOST,
|
||||||
) . '/public'
|
) . '/public',
|
||||||
);
|
);
|
||||||
$socialSignUpelement->addAttribute(
|
$socialSignUpelement->addAttribute(
|
||||||
'signUpUrl',
|
'signUpUrl',
|
||||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||||
(string) $socialPlatform->link_url,
|
(string) $socialPlatform->link_url,
|
||||||
PHP_URL_HOST
|
PHP_URL_HOST,
|
||||||
) . '/auth/sign_up'
|
) . '/auth/sign_up',
|
||||||
);
|
);
|
||||||
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
|
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
|
||||||
'socialSignUp',
|
'socialSignUp',
|
||||||
null,
|
null,
|
||||||
$podcastNamespace
|
RssFeed::PODCAST_NAMESPACE,
|
||||||
);
|
);
|
||||||
$castopodSocialSignUpelement->addAttribute('priority', '1');
|
$castopodSocialSignUpelement->addAttribute('priority', '1');
|
||||||
$castopodSocialSignUpelement->addAttribute(
|
$castopodSocialSignUpelement->addAttribute(
|
||||||
'homeUrl',
|
'homeUrl',
|
||||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||||
(string) $socialPlatform->link_url,
|
(string) $socialPlatform->link_url,
|
||||||
PHP_URL_HOST
|
PHP_URL_HOST,
|
||||||
) . '/public'
|
) . '/public',
|
||||||
);
|
);
|
||||||
$castopodSocialSignUpelement->addAttribute(
|
$castopodSocialSignUpelement->addAttribute(
|
||||||
'signUpUrl',
|
'signUpUrl',
|
||||||
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
|
||||||
(string) $socialPlatform->link_url,
|
(string) $socialPlatform->link_url,
|
||||||
PHP_URL_HOST
|
PHP_URL_HOST,
|
||||||
) . '/auth/sign_up'
|
) . '/auth/sign_up',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,17 +189,15 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$fundingPlatformElement = $channel->addChild(
|
$fundingPlatformElement = $channel->addChild(
|
||||||
'funding',
|
'funding',
|
||||||
$fundingPlatform->account_id,
|
$fundingPlatform->account_id,
|
||||||
$podcastNamespace,
|
RssFeed::PODCAST_NAMESPACE,
|
||||||
);
|
);
|
||||||
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
|
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
|
||||||
if ($fundingPlatform->link_url !== null) {
|
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);
|
||||||
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($podcast->persons as $person) {
|
foreach ($podcast->persons as $person) {
|
||||||
foreach ($person->roles as $role) {
|
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, 'medium'));
|
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
|
||||||
|
|
||||||
|
|
@ -257,29 +226,26 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$channel->addChild(
|
$channel->addChild(
|
||||||
'explicit',
|
'explicit',
|
||||||
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
|
$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);
|
$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) {
|
$channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
|
||||||
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
$channel->addChild('type', $podcast->type, $itunesNamespace);
|
|
||||||
$podcast->copyright &&
|
$podcast->copyright &&
|
||||||
$channel->addChild('copyright', $podcast->copyright);
|
$channel->addChild('copyright', $podcast->copyright);
|
||||||
if ($podcast->is_blocked || $subscription instanceof Subscription) {
|
if ($podcast->is_blocked || $subscription instanceof Subscription) {
|
||||||
$channel->addChild('block', 'Yes', $itunesNamespace);
|
$channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($podcast->is_completed) {
|
if ($podcast->is_completed) {
|
||||||
$channel->addChild('complete', 'Yes', $itunesNamespace);
|
$channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
$image = $channel->addChild('image');
|
$image = $channel->addChild('image');
|
||||||
|
|
@ -287,17 +253,16 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$image->addChild('title', $podcast->title, null, false);
|
$image->addChild('title', $podcast->title, null, false);
|
||||||
$image->addChild('link', $podcast->link);
|
$image->addChild('link', $podcast->link);
|
||||||
|
|
||||||
if ($podcast->custom_rss !== null) {
|
// run plugins hook at the end
|
||||||
array_to_rss([
|
$plugins->rssAfterChannel($podcast, $channel);
|
||||||
'elements' => $podcast->custom_rss,
|
|
||||||
], $channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($episodes as $episode) {
|
foreach ($episodes as $episode) {
|
||||||
if ($episode->is_premium && ! $subscription instanceof Subscription) {
|
if ($episode->is_premium && ! $subscription instanceof Subscription) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$plugins->rssBeforeItem($episode);
|
||||||
|
|
||||||
$item = $channel->addChild('item');
|
$item = $channel->addChild('item');
|
||||||
$item->addChild('title', $episode->title, null, false);
|
$item->addChild('title', $episode->title, null, false);
|
||||||
$enclosure = $item->addChild('enclosure');
|
$enclosure = $item->addChild('enclosure');
|
||||||
|
|
@ -317,7 +282,7 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$item->addChild('guid', $episode->guid);
|
$item->addChild('guid', $episode->guid);
|
||||||
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
|
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
|
||||||
if ($episode->location instanceof Location) {
|
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) {
|
if ($episode->location->geo !== null) {
|
||||||
$locationElement->addAttribute('geo', $episode->location->geo);
|
$locationElement->addAttribute('geo', $episode->location->geo);
|
||||||
}
|
}
|
||||||
|
|
@ -327,10 +292,10 @@ if (! function_exists('get_rss_feed')) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug));
|
$item->addChildWithCDATA('description', $episode->description_html);
|
||||||
$item->addChild('duration', (string) round($episode->audio->duration), $itunesNamespace);
|
$item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
|
||||||
$item->addChild('link', $episode->link);
|
$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);
|
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
|
||||||
|
|
||||||
$episode->parental_advisory &&
|
$episode->parental_advisory &&
|
||||||
|
|
@ -339,18 +304,18 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$episode->parental_advisory === 'explicit'
|
$episode->parental_advisory === 'explicit'
|
||||||
? 'true'
|
? 'true'
|
||||||
: 'false',
|
: 'false',
|
||||||
$itunesNamespace,
|
RssFeed::ITUNES_NAMESPACE,
|
||||||
);
|
);
|
||||||
|
|
||||||
$episode->number &&
|
$episode->number &&
|
||||||
$item->addChild('episode', (string) $episode->number, $itunesNamespace);
|
$item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
|
||||||
$episode->season_number &&
|
$episode->season_number &&
|
||||||
$item->addChild('season', (string) $episode->season_number, $itunesNamespace);
|
$item->addChild('season', (string) $episode->season_number, RssFeed::ITUNES_NAMESPACE);
|
||||||
$item->addChild('episodeType', $episode->type, $itunesNamespace);
|
$item->addChild('episodeType', $episode->type, RssFeed::ITUNES_NAMESPACE);
|
||||||
|
|
||||||
// If episode is of type trailer, add podcast:trailer tag on channel level
|
// If episode is of type trailer, add podcast:trailer tag on channel level
|
||||||
if ($episode->type === 'trailer') {
|
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('pubdate', $episode->published_at->format(DATE_RFC2822));
|
||||||
$trailer->addAttribute(
|
$trailer->addAttribute(
|
||||||
'url',
|
'url',
|
||||||
|
|
@ -363,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
|
// 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('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
|
||||||
$comments->addAttribute('contentType', 'application/podcast-activity+json');
|
$comments->addAttribute('contentType', 'application/podcast-activity+json');
|
||||||
|
|
||||||
if ($episode->getPosts()) {
|
if ($episode->getPosts()) {
|
||||||
$socialInteractUri = $episode->getPosts()[0]
|
$socialInteractUri = $episode->getPosts()[0]
|
||||||
->uri;
|
->uri;
|
||||||
$socialInteractElement = $item->addChild('socialInteract', null, $podcastNamespace);
|
$socialInteractElement = $item->addChild('socialInteract', null, RssFeed::PODCAST_NAMESPACE);
|
||||||
$socialInteractElement->addAttribute('uri', $socialInteractUri);
|
$socialInteractElement->addAttribute('uri', $socialInteractUri);
|
||||||
$socialInteractElement->addAttribute('priority', '1');
|
$socialInteractElement->addAttribute('priority', '1');
|
||||||
$socialInteractElement->addAttribute('platform', 'castopod');
|
$socialInteractElement->addAttribute('platform', 'castopod');
|
||||||
$socialInteractElement->addAttribute('protocol', 'activitypub');
|
$socialInteractElement->addAttribute('protocol', 'activitypub');
|
||||||
$socialInteractElement->addAttribute(
|
$socialInteractElement->addAttribute(
|
||||||
'accountId',
|
'accountId',
|
||||||
"@{$podcast->actor->username}@{$podcast->actor->domain}"
|
"@{$podcast->actor->username}@{$podcast->actor->domain}",
|
||||||
);
|
);
|
||||||
$socialInteractElement->addAttribute(
|
$socialInteractElement->addAttribute(
|
||||||
'pubDate',
|
'pubDate',
|
||||||
$episode->getPosts()[0]
|
$episode->getPosts()[0]
|
||||||
->published_at->format(DateTime::ISO8601)
|
->published_at->format(DateTime::ISO8601),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($episode->transcript instanceof Transcript) {
|
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('url', $episode->transcript->file_url);
|
||||||
$transcriptElement->addAttribute(
|
$transcriptElement->addAttribute(
|
||||||
'type',
|
'type',
|
||||||
Mimes::guessTypeFromExtension(
|
Mimes::guessTypeFromExtension(
|
||||||
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION)
|
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION),
|
||||||
) ?? 'text/html',
|
) ?? 'text/html',
|
||||||
);
|
);
|
||||||
// Castopod only allows for captions (SubRip files)
|
// Castopod only allows for captions (SubRip files)
|
||||||
|
|
@ -409,21 +368,21 @@ if (! function_exists('get_rss_feed')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($episode->getChapters() instanceof Chapters) {
|
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('url', $episode->chapters->file_url);
|
||||||
$chaptersElement->addAttribute('type', 'application/json+chapters');
|
$chaptersElement->addAttribute('type', 'application/json+chapters');
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($episode->soundbites as $soundbite) {
|
foreach ($episode->soundbites as $soundbite) {
|
||||||
// TODO: differentiate video from soundbites?
|
// 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('startTime', (string) $soundbite->start_time);
|
||||||
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
|
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($episode->persons as $person) {
|
foreach ($episode->persons as $person) {
|
||||||
foreach ($person->roles as $role) {
|
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(
|
$personElement->addAttribute(
|
||||||
'role',
|
'role',
|
||||||
|
|
@ -444,14 +403,10 @@ if (! function_exists('get_rss_feed')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($episode->is_blocked) {
|
if ($episode->is_blocked) {
|
||||||
$item->addChild('block', 'Yes', $itunesNamespace);
|
$item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($episode->custom_rss !== null) {
|
$plugins->rssAfterItem($episode, $item);
|
||||||
array_to_rss([
|
|
||||||
'elements' => $episode->custom_rss,
|
|
||||||
], $item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rss->asXML();
|
return $rss->asXML();
|
||||||
|
|
@ -462,11 +417,9 @@ if (! function_exists('add_category_tag')) {
|
||||||
/**
|
/**
|
||||||
* Adds <itunes:category> and <category> tags to node for a given category
|
* 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, RssFeed::ITUNES_NAMESPACE);
|
||||||
|
|
||||||
$itunesCategory = $node->addChild('category', null, $itunesNamespace);
|
|
||||||
$itunesCategory->addAttribute(
|
$itunesCategory->addAttribute(
|
||||||
'text',
|
'text',
|
||||||
$category->parent instanceof Category
|
$category->parent instanceof Category
|
||||||
|
|
@ -475,7 +428,7 @@ if (! function_exists('add_category_tag')) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($category->parent instanceof Category) {
|
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);
|
$itunesCategoryChild->addAttribute('text', $category->apple_category);
|
||||||
$node->addChild('category', $category->parent->apple_category);
|
$node->addChild('category', $category->parent->apple_category);
|
||||||
}
|
}
|
||||||
|
|
@ -483,71 +436,3 @@ if (! function_exists('add_category_tag')) {
|
||||||
$node->addChild('category', $category->apple_category);
|
$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 (strlen($textcontent) > 0) {
|
|
||||||
$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\Page;
|
||||||
use App\Entities\Podcast;
|
use App\Entities\Podcast;
|
||||||
use App\Entities\Post;
|
use App\Entities\Post;
|
||||||
use Melbahja\Seo\MetaTags;
|
use App\Libraries\HtmlHead;
|
||||||
use Melbahja\Seo\Schema;
|
use Melbahja\Seo\Schema;
|
||||||
use Melbahja\Seo\Schema\Thing;
|
use Melbahja\Seo\Schema\Thing;
|
||||||
use Modules\Fediverse\Entities\PreviewCard;
|
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
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (! function_exists('get_podcast_metatags')) {
|
if (! function_exists('set_podcast_metatags')) {
|
||||||
function get_podcast_metatags(Podcast $podcast, string $page): string
|
function set_podcast_metatags(Podcast $podcast, string $page): void
|
||||||
{
|
{
|
||||||
$category = '';
|
$category = '';
|
||||||
if ($podcast->category->parent_id !== null) {
|
if ($podcast->category->parent_id !== null) {
|
||||||
|
|
@ -30,28 +30,32 @@ if (! function_exists('get_podcast_metatags')) {
|
||||||
$category .= $podcast->category->apple_category;
|
$category .= $podcast->category->apple_category;
|
||||||
|
|
||||||
$schema = new Schema(
|
$schema = new Schema(
|
||||||
new Thing('PodcastSeries', [
|
new Thing(
|
||||||
'name' => $podcast->title,
|
props: [
|
||||||
'headline' => $podcast->title,
|
'name' => $podcast->title,
|
||||||
'url' => current_url(),
|
'headline' => $podcast->title,
|
||||||
'sameAs' => $podcast->link,
|
'url' => current_url(),
|
||||||
'identifier' => $podcast->guid,
|
'sameAs' => $podcast->link,
|
||||||
'image' => $podcast->cover->feed_url,
|
'identifier' => $podcast->guid,
|
||||||
'description' => $podcast->description,
|
'image' => $podcast->cover->feed_url,
|
||||||
'webFeed' => $podcast->feed_url,
|
'description' => $podcast->description,
|
||||||
'accessMode' => 'auditory',
|
'webFeed' => $podcast->feed_url,
|
||||||
'author' => $podcast->owner_name,
|
'accessMode' => 'auditory',
|
||||||
'creator' => $podcast->owner_name,
|
'author' => $podcast->owner_name,
|
||||||
'publisher' => $podcast->publisher,
|
'creator' => $podcast->owner_name,
|
||||||
'inLanguage' => $podcast->language_code,
|
'publisher' => $podcast->publisher,
|
||||||
'genre' => $category,
|
'inLanguage' => $podcast->language_code,
|
||||||
])
|
'genre' => $category,
|
||||||
|
],
|
||||||
|
type: 'PodcastSeries',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
|
$head = service('html_head');
|
||||||
|
|
||||||
$metatags
|
$head
|
||||||
->title($podcast->title . ' (@' . $podcast->handle . ') • ' . lang('Podcast.' . $page))
|
->title(sprintf('%s (@%s) • %s', $podcast->title, $podcast->handle, lang('Podcast.' . $page)))
|
||||||
->description(esc($podcast->description))
|
->description(esc($podcast->description))
|
||||||
->image((string) $podcast->cover->og_url)
|
->image((string) $podcast->cover->og_url)
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
|
|
@ -59,47 +63,51 @@ if (! function_exists('get_podcast_metatags')) {
|
||||||
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
|
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
|
||||||
->og('locale', $podcast->language_code)
|
->og('locale', $podcast->language_code)
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||||
->push('link', [
|
->tag('link', null, [
|
||||||
'rel' => 'alternate',
|
'rel' => 'alternate',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
'href' => url_to('podcast-activity', esc($podcast->handle)),
|
'href' => url_to('podcast-activity', esc($podcast->handle)),
|
||||||
]);
|
])->appendRawContent('<link type="application/rss+xml" rel="alternate" title="' . esc(
|
||||||
|
$podcast->title,
|
||||||
if ($podcast->payment_pointer) {
|
) . '" href="' . $podcast->feed_url . '" />' . $schema);
|
||||||
$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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_episode_metatags')) {
|
if (! function_exists('set_episode_metatags')) {
|
||||||
function get_episode_metatags(Episode $episode): string
|
function set_episode_metatags(Episode $episode): void
|
||||||
{
|
{
|
||||||
$schema = new Schema(
|
$schema = new Schema(
|
||||||
new Thing('PodcastEpisode', [
|
new Thing(
|
||||||
'url' => url_to('episode', esc($episode->podcast->handle), $episode->slug),
|
props: [
|
||||||
'name' => $episode->title,
|
'url' => url_to('episode', esc($episode->podcast->handle), $episode->slug),
|
||||||
'image' => $episode->cover->feed_url,
|
'name' => $episode->title,
|
||||||
'description' => $episode->description,
|
'image' => $episode->cover->feed_url,
|
||||||
'datePublished' => $episode->published_at->format(DATE_ISO8601),
|
'description' => $episode->description,
|
||||||
'timeRequired' => iso8601_duration($episode->audio->duration),
|
'datePublished' => $episode->published_at->format(DATE_ATOM),
|
||||||
'duration' => iso8601_duration($episode->audio->duration),
|
'timeRequired' => iso8601_duration($episode->audio->duration),
|
||||||
'associatedMedia' => new Thing('MediaObject', [
|
'duration' => iso8601_duration($episode->audio->duration),
|
||||||
'contentUrl' => $episode->audio_url,
|
'associatedMedia' => new Thing(
|
||||||
]),
|
props: [
|
||||||
'partOfSeries' => new Thing('PodcastSeries', [
|
'contentUrl' => $episode->audio_url,
|
||||||
'name' => $episode->podcast->title,
|
],
|
||||||
'url' => $episode->podcast->link,
|
type: 'MediaObject',
|
||||||
]),
|
),
|
||||||
])
|
'partOfSeries' => new Thing(
|
||||||
|
props: [
|
||||||
|
'name' => $episode->podcast->title,
|
||||||
|
'url' => $episode->podcast->link,
|
||||||
|
],
|
||||||
|
type: 'PodcastSeries',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
type: 'PodcastEpisode',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
|
$head = service('html_head');
|
||||||
|
|
||||||
$metatags
|
$head
|
||||||
->title($episode->title)
|
->title($episode->title)
|
||||||
->description(esc($episode->description))
|
->description(esc($episode->description))
|
||||||
->image((string) $episode->cover->og_url, 'player')
|
->image((string) $episode->cover->og_url, 'player')
|
||||||
|
|
@ -110,68 +118,83 @@ if (! function_exists('get_episode_metatags')) {
|
||||||
->og('locale', $episode->podcast->language_code)
|
->og('locale', $episode->podcast->language_code)
|
||||||
->og('audio', $episode->audio_opengraph_url)
|
->og('audio', $episode->audio_opengraph_url)
|
||||||
->og('audio:type', $episode->audio->file_mimetype)
|
->og('audio:type', $episode->audio->file_mimetype)
|
||||||
->meta('article:published_time', $episode->published_at->format(DATE_ISO8601))
|
->meta('article:published_time', $episode->published_at->format(DATE_ATOM))
|
||||||
->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601))
|
->meta('article:modified_time', $episode->updated_at->format(DATE_ATOM))
|
||||||
->twitter('audio:partner', $episode->podcast->publisher ?? '')
|
->twitter('audio:partner', $episode->podcast->publisher ?? '')
|
||||||
->twitter('audio:artist_name', esc($episode->podcast->owner_name))
|
->twitter('audio:artist_name', esc($episode->podcast->owner_name))
|
||||||
->twitter('player', $episode->getEmbedUrl('light'))
|
->twitter('player', $episode->getEmbedUrl('light'))
|
||||||
->twitter('player:width', (string) config('Embed')->width)
|
->twitter('player:width', (string) config('Embed')->width)
|
||||||
->twitter('player:height', (string) config('Embed')->height)
|
->twitter('player:height', (string) config('Embed')->height)
|
||||||
->push('link', [
|
->tag('link', null, [
|
||||||
'rel' => 'alternate',
|
'rel' => 'alternate',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
'href' => url_to('episode', $episode->podcast->handle, $episode->slug),
|
'href' => $episode->link,
|
||||||
]);
|
])
|
||||||
|
->appendRawContent('<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
||||||
if ($episode->podcast->payment_pointer) {
|
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug),
|
||||||
$metatags->meta('monetization', $episode->podcast->payment_pointer);
|
) . '" title="' . esc(
|
||||||
}
|
$episode->title,
|
||||||
|
) . ' oEmbed json" />' . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
|
||||||
return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug),
|
||||||
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
|
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . $schema);
|
||||||
) . '" 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_post_metatags')) {
|
if (! function_exists('set_post_metatags')) {
|
||||||
function get_post_metatags(Post $post): string
|
function set_post_metatags(Post $post): void
|
||||||
{
|
{
|
||||||
$socialMediaPosting = new Thing('SocialMediaPosting', [
|
$socialMediaPosting = new Thing(
|
||||||
'@id' => url_to('post', esc($post->actor->username), $post->id),
|
props: [
|
||||||
'datePublished' => $post->published_at->format(DATE_ISO8601),
|
'@id' => url_to('post', esc($post->actor->username), $post->id),
|
||||||
'author' => new Thing('Person', [
|
'datePublished' => $post->published_at->format(DATE_ATOM),
|
||||||
'name' => $post->actor->display_name,
|
'author' => new Thing(
|
||||||
'url' => $post->actor->uri,
|
props: [
|
||||||
]),
|
'name' => $post->actor->display_name,
|
||||||
'text' => $post->message,
|
'url' => $post->actor->uri,
|
||||||
]);
|
],
|
||||||
|
type: 'Person',
|
||||||
|
),
|
||||||
|
'text' => $post->message,
|
||||||
|
],
|
||||||
|
type: 'SocialMediaPosting',
|
||||||
|
);
|
||||||
|
|
||||||
if ($post->episode_id !== null) {
|
if ($post->episode_id !== null) {
|
||||||
$socialMediaPosting->__set('sharedContent', new Thing('Audio', [
|
$socialMediaPosting->__set('sharedContent', new Thing(
|
||||||
'headline' => $post->episode->title,
|
props: [
|
||||||
'url' => $post->episode->link,
|
'headline' => $post->episode->title,
|
||||||
'author' => new Thing('Person', [
|
'url' => $post->episode->link,
|
||||||
'name' => $post->episode->podcast->owner_name,
|
'author' => new Thing(
|
||||||
]),
|
props: [
|
||||||
]));
|
'name' => $post->episode->podcast->owner_name,
|
||||||
|
],
|
||||||
|
type: 'Person',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
type: 'Audio',
|
||||||
|
));
|
||||||
} elseif ($post->preview_card instanceof PreviewCard) {
|
} elseif ($post->preview_card instanceof PreviewCard) {
|
||||||
$socialMediaPosting->__set('sharedContent', new Thing('WebPage', [
|
$socialMediaPosting->__set('sharedContent', new Thing(
|
||||||
'headline' => $post->preview_card->title,
|
props: [
|
||||||
'url' => $post->preview_card->url,
|
'headline' => $post->preview_card->title,
|
||||||
'author' => new Thing('Person', [
|
'url' => $post->preview_card->url,
|
||||||
'name' => $post->preview_card->author_name,
|
'author' => new Thing(
|
||||||
]),
|
props: [
|
||||||
]));
|
'name' => $post->preview_card->author_name,
|
||||||
|
],
|
||||||
|
type: 'Person',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
type: 'WebPage',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
$schema = new Schema($socialMediaPosting);
|
$schema = new Schema($socialMediaPosting);
|
||||||
|
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
|
||||||
|
$head
|
||||||
->title(lang('Post.title', [
|
->title(lang('Post.title', [
|
||||||
'actorDisplayName' => $post->actor->display_name,
|
'actorDisplayName' => $post->actor->display_name,
|
||||||
]))
|
]))
|
||||||
|
|
@ -179,37 +202,43 @@ if (! function_exists('get_post_metatags')) {
|
||||||
->image($post->actor->avatar_image_url)
|
->image($post->actor->avatar_image_url)
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||||
->push('link', [
|
->tag('link', null, [
|
||||||
'rel' => 'alternate',
|
'rel' => 'alternate',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
'href' => url_to('post', esc($post->actor->username), $post->id),
|
'href' => url_to('post', esc($post->actor->username), $post->id),
|
||||||
]);
|
])->appendRawContent((string) $schema);
|
||||||
|
|
||||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_episode_comment_metatags')) {
|
if (! function_exists('set_episode_comment_metatags')) {
|
||||||
function get_episode_comment_metatags(EpisodeComment $episodeComment): string
|
function set_episode_comment_metatags(EpisodeComment $episodeComment): void
|
||||||
{
|
{
|
||||||
$schema = new Schema(new Thing('SocialMediaPosting', [
|
$schema = new Schema(new Thing(
|
||||||
'@id' => url_to(
|
props: [
|
||||||
'episode-comment',
|
'@id' => url_to(
|
||||||
esc($episodeComment->actor->username),
|
'episode-comment',
|
||||||
$episodeComment->episode->slug,
|
esc($episodeComment->actor->username),
|
||||||
$episodeComment->id
|
$episodeComment->episode->slug,
|
||||||
),
|
$episodeComment->id,
|
||||||
'datePublished' => $episodeComment->created_at->format(DATE_ISO8601),
|
),
|
||||||
'author' => new Thing('Person', [
|
'datePublished' => $episodeComment->created_at->format(DATE_ATOM),
|
||||||
'name' => $episodeComment->actor->display_name,
|
'author' => new Thing(
|
||||||
'url' => $episodeComment->actor->uri,
|
props: [
|
||||||
]),
|
'name' => $episodeComment->actor->display_name,
|
||||||
'text' => $episodeComment->message,
|
'url' => $episodeComment->actor->uri,
|
||||||
'upvoteCount' => $episodeComment->likes_count,
|
],
|
||||||
]));
|
type: 'Person',
|
||||||
|
),
|
||||||
|
'text' => $episodeComment->message,
|
||||||
|
'upvoteCount' => $episodeComment->likes_count,
|
||||||
|
],
|
||||||
|
type: 'SocialMediaPosting',
|
||||||
|
));
|
||||||
|
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
|
||||||
|
$head
|
||||||
->title(lang('Comment.title', [
|
->title(lang('Comment.title', [
|
||||||
'actorDisplayName' => $episodeComment->actor->display_name,
|
'actorDisplayName' => $episodeComment->actor->display_name,
|
||||||
'episodeTitle' => $episodeComment->episode->title,
|
'episodeTitle' => $episodeComment->episode->title,
|
||||||
|
|
@ -218,26 +247,25 @@ if (! function_exists('get_episode_comment_metatags')) {
|
||||||
->image($episodeComment->actor->avatar_image_url)
|
->image($episodeComment->actor->avatar_image_url)
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||||
->push('link', [
|
->tag('link', null, [
|
||||||
'rel' => 'alternate',
|
'rel' => 'alternate',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
'href' => url_to(
|
'href' => url_to(
|
||||||
'episode-comment',
|
'episode-comment',
|
||||||
$episodeComment->actor->username,
|
$episodeComment->actor->username,
|
||||||
$episodeComment->episode->slug,
|
$episodeComment->episode->slug,
|
||||||
$episodeComment->id
|
$episodeComment->id,
|
||||||
),
|
),
|
||||||
]);
|
])->appendRawContent((string) $schema);
|
||||||
|
|
||||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_follow_metatags')) {
|
if (! function_exists('set_follow_metatags')) {
|
||||||
function get_follow_metatags(Actor $actor): string
|
function set_follow_metatags(Actor $actor): void
|
||||||
{
|
{
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
$head
|
||||||
->title(lang('Podcast.followTitle', [
|
->title(lang('Podcast.followTitle', [
|
||||||
'actorDisplayName' => $actor->display_name,
|
'actorDisplayName' => $actor->display_name,
|
||||||
]))
|
]))
|
||||||
|
|
@ -245,16 +273,15 @@ if (! function_exists('get_follow_metatags')) {
|
||||||
->image($actor->avatar_image_url)
|
->image($actor->avatar_image_url)
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||||
|
|
||||||
return $metatags->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_remote_actions_metatags')) {
|
if (! function_exists('set_remote_actions_metatags')) {
|
||||||
function get_remote_actions_metatags(Post $post, string $action): string
|
function set_remote_actions_metatags(Post $post, string $action): void
|
||||||
{
|
{
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
$head
|
||||||
->title(lang('Fediverse.' . $action . '.title', [
|
->title(lang('Fediverse.' . $action . '.title', [
|
||||||
'actorDisplayName' => $post->actor->display_name,
|
'actorDisplayName' => $post->actor->display_name,
|
||||||
],))
|
],))
|
||||||
|
|
@ -262,42 +289,40 @@ if (! function_exists('get_remote_actions_metatags')) {
|
||||||
->image($post->actor->avatar_image_url)
|
->image($post->actor->avatar_image_url)
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||||
|
|
||||||
return $metatags->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_home_metatags')) {
|
if (! function_exists('set_home_metatags')) {
|
||||||
function get_home_metatags(): string
|
function set_home_metatags(): void
|
||||||
{
|
{
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
$head
|
||||||
->title(service('settings')->get('App.siteName'))
|
->title(service('settings')->get('App.siteName'))
|
||||||
->description(esc(service('settings')->get('App.siteDescription')))
|
->description(esc(service('settings')->get('App.siteDescription')))
|
||||||
->image(get_site_icon_url('512'))
|
->image(get_site_icon_url('512'))
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||||
|
|
||||||
return $metatags->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('get_page_metatags')) {
|
if (! function_exists('set_page_metatags')) {
|
||||||
function get_page_metatags(Page $page): string
|
function set_page_metatags(Page $page): void
|
||||||
{
|
{
|
||||||
$metatags = new MetaTags();
|
/** @var HtmlHead $head */
|
||||||
$metatags
|
$head = service('html_head');
|
||||||
|
$head
|
||||||
->title(
|
->title(
|
||||||
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
|
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
|
||||||
'settings'
|
'settings',
|
||||||
)->get('App.siteName')
|
)->get('App.siteName'),
|
||||||
)
|
)
|
||||||
->description(esc(service('settings')->get('App.siteDescription')))
|
->description(esc(service('settings')->get('App.siteDescription')))
|
||||||
->image(get_site_icon_url('512'))
|
->image(get_site_icon_url('512'))
|
||||||
->canonical((string) current_url())
|
->canonical((string) current_url())
|
||||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||||
|
|
||||||
return $metatags->__toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ return [
|
||||||
'comments' => 'Kommentare',
|
'comments' => 'Kommentare',
|
||||||
'activity' => 'Aktivitäten',
|
'activity' => 'Aktivitäten',
|
||||||
'chapters' => 'Kapitel',
|
'chapters' => 'Kapitel',
|
||||||
'transcript' => 'Transcript',
|
'transcript' => 'Protokoll',
|
||||||
'description' => 'Beschreibung der Episode',
|
'description' => 'Beschreibung der Episode',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# Kommentar}
|
one {# Kommentar}
|
||||||
|
|
@ -45,6 +45,6 @@ return [
|
||||||
'publish_edit' => 'Veröffentlichung bearbeiten',
|
'publish_edit' => 'Veröffentlichung bearbeiten',
|
||||||
],
|
],
|
||||||
'no_chapters' => 'Für diese Episode sind keine Kapitel verfügbar.',
|
'no_chapters' => 'Für diese Episode sind keine Kapitel verfügbar.',
|
||||||
'download_transcript' => 'Download transcript ({extension})',
|
'download_transcript' => 'Protokoll herunterladen ({extension})',
|
||||||
'no_transcript' => 'No transcript available for this episode.',
|
'no_transcript' => 'Für diese Episode ist kein Protokoll verfügbar.',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ declare(strict_types=1);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'your_handle' => 'Your handle',
|
'your_handle' => 'Your Fediverse handle',
|
||||||
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
|
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
|
||||||
'follow' => [
|
'follow' => [
|
||||||
'label' => 'Follow',
|
'label' => 'Follow',
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ return [
|
||||||
'no_episode' => 'No episode found!',
|
'no_episode' => 'No episode found!',
|
||||||
'follow' => 'Follow',
|
'follow' => 'Follow',
|
||||||
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
|
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
|
||||||
'followers' => '{numberOfFollowers, plural,
|
'fediverseFollowers' => '{numberOfFollowers, plural,
|
||||||
one {# follower}
|
one {# Fediverse follower}
|
||||||
other {# followers}
|
other {# Fediverse followers}
|
||||||
}',
|
}',
|
||||||
'posts' => '{numberOfPosts, plural,
|
'posts' => '{numberOfPosts, plural,
|
||||||
one {# post}
|
one {# post}
|
||||||
|
|
@ -42,7 +42,7 @@ return [
|
||||||
}',
|
}',
|
||||||
'first_published_at' => 'First episode published on {0, date, medium}',
|
'first_published_at' => 'First episode published on {0, date, medium}',
|
||||||
],
|
],
|
||||||
'sponsor' => 'Sponsor',
|
'funding' => 'Funding',
|
||||||
'funding_links' => 'Funding links for {podcastTitle}',
|
'funding_links' => 'Funding links for {podcastTitle}',
|
||||||
'find_on' => 'Find {podcastTitle} on',
|
'find_on' => 'Find {podcastTitle} on',
|
||||||
'listen_on' => 'Listen on',
|
'listen_on' => 'Listen on',
|
||||||
|
|
@ -51,5 +51,5 @@ return [
|
||||||
other {# persons}
|
other {# persons}
|
||||||
}',
|
}',
|
||||||
'persons_list' => 'Persons',
|
'persons_list' => 'Persons',
|
||||||
'castopod_website' => 'Castopod (website)',
|
'links_mainpage' => 'Podcast (main page)',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,7 @@ return [
|
||||||
'block_actor' => 'Block user @{actorUsername}',
|
'block_actor' => 'Block user @{actorUsername}',
|
||||||
'block_domain' => 'Block domain @{actorDomain}',
|
'block_domain' => 'Block domain @{actorDomain}',
|
||||||
'delete' => 'Delete post',
|
'delete' => 'Delete post',
|
||||||
|
'is_public' => 'Post is public',
|
||||||
|
'is_private' => 'Post is private',
|
||||||
|
'cannot_reblog' => 'This private post cannot be shared.',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ return [
|
||||||
'comments' => 'コメント',
|
'comments' => 'コメント',
|
||||||
'activity' => 'アクティビティ',
|
'activity' => 'アクティビティ',
|
||||||
'chapters' => '章',
|
'chapters' => '章',
|
||||||
'transcript' => 'Transcript',
|
'transcript' => '文字起こし',
|
||||||
'description' => 'エピソードの詳細',
|
'description' => 'エピソードの詳細',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# comment}
|
one {# comment}
|
||||||
|
|
@ -43,7 +43,7 @@ return [
|
||||||
'publish' => '公開する',
|
'publish' => '公開する',
|
||||||
'publish_edit' => '出版物を編集',
|
'publish_edit' => '出版物を編集',
|
||||||
],
|
],
|
||||||
'no_chapters' => 'No chapters are available for this episode.',
|
'no_chapters' => 'このエピソードにはチャプターがありません。',
|
||||||
'download_transcript' => 'Download transcript ({extension})',
|
'download_transcript' => '文字起こしをダウンロード ({extension})',
|
||||||
'no_transcript' => 'No transcript available for this episode.',
|
'no_transcript' => 'このエピソードには文字起こしがありません。',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -10,28 +10,28 @@ declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'your_handle' => 'あなたのユーザー ID',
|
'your_handle' => 'あなたのユーザー ID',
|
||||||
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
|
'your_handle_hint' => 'フォームに「@username@domain」の形式で入力してください',
|
||||||
'follow' => [
|
'follow' => [
|
||||||
'label' => 'フォロー',
|
'label' => 'フォロー',
|
||||||
'title' => '{actorDisplayName} をフォロー',
|
'title' => '{actorDisplayName} をフォロー',
|
||||||
'subtitle' => 'You are going to follow:',
|
'subtitle' => 'フォロー中:',
|
||||||
'accountNotFound' => 'アカウントが見つかりませんでした',
|
'accountNotFound' => 'アカウントが見つかりませんでした',
|
||||||
'remoteFollowNotAllowed' => 'このアカウントサーバーはリモートフォローを許可しておりません',
|
'remoteFollowNotAllowed' => 'このアカウントサーバーはリモートフォローを許可しておりません',
|
||||||
'submit' => 'フォローする',
|
'submit' => 'フォローする',
|
||||||
],
|
],
|
||||||
'favourite' => [
|
'favourite' => [
|
||||||
'title' => "お気に入りの {actorDisplayName}の投稿",
|
'title' => "お気に入りの {actorDisplayName}の投稿",
|
||||||
'subtitle' => 'You are going to favourite:',
|
'subtitle' => 'お気に入りに登録中:',
|
||||||
'submit' => 'お気に入り登録する',
|
'submit' => 'お気に入り登録する',
|
||||||
],
|
],
|
||||||
'reblog' => [
|
'reblog' => [
|
||||||
'title' => "Share {actorDisplayName}'s post",
|
'title' => "{actorDisplayName} の投稿を共有する",
|
||||||
'subtitle' => 'You are going to share:',
|
'subtitle' => '共有中:',
|
||||||
'submit' => '共有する',
|
'submit' => '共有する',
|
||||||
],
|
],
|
||||||
'reply' => [
|
'reply' => [
|
||||||
'title' => "Reply to {actorDisplayName}'s post",
|
'title' => "{actorDisplayName} の投稿に返信する",
|
||||||
'subtitle' => 'You are going to reply to:',
|
'subtitle' => '返信中:',
|
||||||
'submit' => '返信する',
|
'submit' => '返信する',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,16 @@ return [
|
||||||
one {# episode}
|
one {# episode}
|
||||||
other {# episodes}
|
other {# episodes}
|
||||||
}',
|
}',
|
||||||
'first_published_at' => 'First episode published on {0, date, medium}',
|
'first_published_at' => '初回は{0, date, medium} に投稿されました。',
|
||||||
],
|
],
|
||||||
'sponsor' => 'Sponsor',
|
'sponsor' => 'スポンサー',
|
||||||
'funding_links' => 'Funding links for {podcastTitle}',
|
'funding_links' => '{podcastTitle} のリンクを探す',
|
||||||
'find_on' => 'Find {podcastTitle} on',
|
'find_on' => '{podcastTitle} を検索',
|
||||||
'listen_on' => 'Listen on',
|
'listen_on' => '視聴中',
|
||||||
'persons' => '{personsCount, plural,
|
'persons' => '{personsCount, plural,
|
||||||
one {# person}
|
one {# person}
|
||||||
other {# persons}
|
other {# persons}
|
||||||
}',
|
}',
|
||||||
'persons_list' => 'Persons',
|
'persons_list' => '人数',
|
||||||
'castopod_website' => 'Castopod (website)',
|
'castopod_website' => 'Castopod (公式ページ)',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ declare(strict_types=1);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => "{actorDisplayName}'s post",
|
'title' => "{actorDisplayName} の投稿",
|
||||||
'back_to_actor_posts' => 'Back to {actor} posts',
|
'back_to_actor_posts' => '{actor} の投稿一覧に戻る',
|
||||||
'actor_shared' => '{actor} shared',
|
'actor_shared' => '{actor} が共有しました',
|
||||||
'reply_to' => 'Reply to @{actorUsername}',
|
'reply_to' => '@{actorUsername} に返信する',
|
||||||
'form' => [
|
'form' => [
|
||||||
'message_placeholder' => 'Write a message…',
|
'message_placeholder' => 'ここにコメントを入力..',
|
||||||
'episode_message_placeholder' => 'Write a message for the episode…',
|
'episode_message_placeholder' => 'エピソードへのコメントを入力...',
|
||||||
'episode_url_placeholder' => 'Episode URL',
|
'episode_url_placeholder' => 'エピソードのURL',
|
||||||
'reply_to_placeholder' => 'Reply to @{actorUsername}',
|
'reply_to_placeholder' => '@{actorUsername} に返信する',
|
||||||
'submit' => '送信',
|
'submit' => '送信',
|
||||||
'submit_reply' => '返信する',
|
'submit_reply' => '返信する',
|
||||||
],
|
],
|
||||||
|
|
@ -33,8 +33,8 @@ return [
|
||||||
one {# reply}
|
one {# reply}
|
||||||
other {# replies}
|
other {# replies}
|
||||||
}',
|
}',
|
||||||
'expand' => 'Expand post',
|
'expand' => '投稿を開く',
|
||||||
'block_actor' => 'Block user @{actorUsername}',
|
'block_actor' => '@{actorUsername} をブロック',
|
||||||
'block_domain' => 'Block domain @{actorDomain}',
|
'block_domain' => '@{actorDomain} の投稿をブロックする',
|
||||||
'delete' => '投稿を削除',
|
'delete' => '投稿を削除',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ return [
|
||||||
'comments' => 'Reacties',
|
'comments' => 'Reacties',
|
||||||
'activity' => 'Activiteiten',
|
'activity' => 'Activiteiten',
|
||||||
'chapters' => 'Hoofdstukken',
|
'chapters' => 'Hoofdstukken',
|
||||||
'transcript' => 'Transcript',
|
'transcript' => 'Transcriptie',
|
||||||
'description' => 'Omschrijving aflevering',
|
'description' => 'Omschrijving aflevering',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# reactie}
|
one {# reactie}
|
||||||
|
|
@ -45,6 +45,6 @@ return [
|
||||||
'publish_edit' => 'Publicatie bewerken',
|
'publish_edit' => 'Publicatie bewerken',
|
||||||
],
|
],
|
||||||
'no_chapters' => 'Voor deze aflevering zijn geen hoofdstukken beschikbaar.',
|
'no_chapters' => 'Voor deze aflevering zijn geen hoofdstukken beschikbaar.',
|
||||||
'download_transcript' => 'Download transcript ({extension})',
|
'download_transcript' => 'Transcriptie downloaden ({extension})',
|
||||||
'no_transcript' => 'No transcript available for this episode.',
|
'no_transcript' => 'Geen transcript beschikbaar voor deze aflevering.',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ return [
|
||||||
'comments' => 'Kommentarar',
|
'comments' => 'Kommentarar',
|
||||||
'activity' => 'Aktivitet',
|
'activity' => 'Aktivitet',
|
||||||
'chapters' => 'Kapittel',
|
'chapters' => 'Kapittel',
|
||||||
'transcript' => 'Transcript',
|
'transcript' => 'Avskrift',
|
||||||
'description' => 'Skildring av episoden',
|
'description' => 'Skildring av episoden',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# kommentar}
|
one {# kommentar}
|
||||||
|
|
@ -45,6 +45,6 @@ return [
|
||||||
'publish_edit' => 'Rediger publiseringa',
|
'publish_edit' => 'Rediger publiseringa',
|
||||||
],
|
],
|
||||||
'no_chapters' => 'Det finst ingen kapittel for denne episoden.',
|
'no_chapters' => 'Det finst ingen kapittel for denne episoden.',
|
||||||
'download_transcript' => 'Download transcript ({extension})',
|
'download_transcript' => 'Last ned underteksten ({extension})',
|
||||||
'no_transcript' => 'No transcript available for this episode.',
|
'no_transcript' => 'Det finst inga teksting for denne episoden.',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,5 @@ return [
|
||||||
'view_replies' => 'Zobacz odpowiedzi ({numberOfReplies})',
|
'view_replies' => 'Zobacz odpowiedzi ({numberOfReplies})',
|
||||||
'block_actor' => 'Zablokuj użytkownika @{actorUsername}',
|
'block_actor' => 'Zablokuj użytkownika @{actorUsername}',
|
||||||
'block_domain' => 'Zablokuj domenę @{actorDomain}',
|
'block_domain' => 'Zablokuj domenę @{actorDomain}',
|
||||||
'delete' => 'usuń komentarz',
|
'delete' => 'Usuń komentarz',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ return [
|
||||||
'cancel' => 'Anuluj',
|
'cancel' => 'Anuluj',
|
||||||
'optional' => 'Opcjonalnie',
|
'optional' => 'Opcjonalnie',
|
||||||
'close' => 'Zamknij',
|
'close' => 'Zamknij',
|
||||||
'home' => 'Początek',
|
'home' => 'Strona główna',
|
||||||
'explicit' => 'Zawiera treści dla dorosłych',
|
'explicit' => 'Wulgarne',
|
||||||
'powered_by' => 'Wspierane przez {castopod}',
|
'powered_by' => 'Wspierane przez {castopod}',
|
||||||
'go_back' => 'Wróć',
|
'go_back' => 'Wróć',
|
||||||
'play_episode_button' => [
|
'play_episode_button' => [
|
||||||
'play' => 'Odtwórz',
|
'play' => 'Odtwórz',
|
||||||
'playing' => 'Odtwarzanie',
|
'playing' => 'Odtwarzanie',
|
||||||
],
|
],
|
||||||
'read_more' => 'czytaj więcej',
|
'read_more' => 'Czytaj więcej',
|
||||||
'read_less' => 'Czytaj mniej',
|
'read_less' => 'Czytaj mniej',
|
||||||
'see_more' => 'Zobacz więcej',
|
'see_more' => 'Zobacz więcej',
|
||||||
'see_less' => 'Zobacz mniej',
|
'see_less' => 'Zobacz mniej',
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ return [
|
||||||
'number' => 'Odcinek {episodeNumber}',
|
'number' => 'Odcinek {episodeNumber}',
|
||||||
'number_abbr' => 'Odc. {episodeNumber}',
|
'number_abbr' => 'Odc. {episodeNumber}',
|
||||||
'season_episode' => 'Sezon {seasonNumber} odcinek {episodeNumber}',
|
'season_episode' => 'Sezon {seasonNumber} odcinek {episodeNumber}',
|
||||||
'season_episode_abbr' => 'S{seasonNumber}:O{episodeNumber}',
|
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
|
||||||
'persons' => '{personsCount, plural,
|
'persons' => '{personsCount, plural,
|
||||||
one {# osoba}
|
one {# osoba}
|
||||||
few {# osoby}
|
few {# osoby}
|
||||||
|
|
@ -24,8 +24,8 @@ return [
|
||||||
'back_to_episodes' => 'Wróć do odcinków {podcast}',
|
'back_to_episodes' => 'Wróć do odcinków {podcast}',
|
||||||
'comments' => 'Komentarze',
|
'comments' => 'Komentarze',
|
||||||
'activity' => 'Aktywność',
|
'activity' => 'Aktywność',
|
||||||
'chapters' => 'Chapters',
|
'chapters' => 'Rozdziały',
|
||||||
'transcript' => 'Transcript',
|
'transcript' => 'Transkrypcja',
|
||||||
'description' => 'Opis odcinka',
|
'description' => 'Opis odcinka',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# komentarz}
|
one {# komentarz}
|
||||||
|
|
@ -33,7 +33,7 @@ return [
|
||||||
other {# komentarzy}
|
other {# komentarzy}
|
||||||
}',
|
}',
|
||||||
'all_podcast_episodes' => 'Wszystkie odcinki podcastu',
|
'all_podcast_episodes' => 'Wszystkie odcinki podcastu',
|
||||||
'back_to_podcast' => 'Wróć do podkastu',
|
'back_to_podcast' => 'Wróć do podcastu',
|
||||||
'preview' => [
|
'preview' => [
|
||||||
'title' => 'Podgląd',
|
'title' => 'Podgląd',
|
||||||
'not_published' => 'Nieopublikowany',
|
'not_published' => 'Nieopublikowany',
|
||||||
|
|
@ -46,7 +46,7 @@ return [
|
||||||
'publish' => 'Opublikuj',
|
'publish' => 'Opublikuj',
|
||||||
'publish_edit' => 'Edytuj publikację',
|
'publish_edit' => 'Edytuj publikację',
|
||||||
],
|
],
|
||||||
'no_chapters' => 'No chapters are available for this episode.',
|
'no_chapters' => 'Brak dostępnych rozdziałów dla tego odcinka.',
|
||||||
'download_transcript' => 'Download transcript ({extension})',
|
'download_transcript' => 'Pobierz transkrypcję ({extension})',
|
||||||
'no_transcript' => 'No transcript available for this episode.',
|
'no_transcript' => 'Brak transkrypcji dla tego odcinka.',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -10,28 +10,28 @@ declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'your_handle' => 'Twój uchwyt',
|
'your_handle' => 'Twój uchwyt',
|
||||||
'your_handle_hint' => 'Wpisz @nazwęużytkownika@domenę, z których chcesz działać.',
|
'your_handle_hint' => 'Wpisz @nazważytkownika@domena, z której chcesz działać.',
|
||||||
'follow' => [
|
'follow' => [
|
||||||
'label' => 'Obserwuj',
|
'label' => 'Obserwuj',
|
||||||
'title' => 'Obserwuj {actorDisplayName}',
|
'title' => 'Obserwuj {actorDisplayName}',
|
||||||
'subtitle' => 'Zamierzasz obserwować:',
|
'subtitle' => 'Zamierzasz obserwować:',
|
||||||
'accountNotFound' => 'Nie można znaleźć konta.',
|
'accountNotFound' => 'Nie można znaleźć konta.',
|
||||||
'remoteFollowNotAllowed' => 'Wygląda na to, że serwer kont nie pozwala na śledzenie zdalnie…',
|
'remoteFollowNotAllowed' => 'Wygląda na to, że serwer kont nie pozwala na zdalne śledzenie…',
|
||||||
'submit' => 'Przejdź do obserwowania',
|
'submit' => 'Przejdź do obserwowania',
|
||||||
],
|
],
|
||||||
'favourite' => [
|
'favourite' => [
|
||||||
'title' => "Dodaj do ulubionych wpis {actorDisplayName}",
|
'title' => "Dodaj do ulubionych wpis {actorDisplayName}",
|
||||||
'subtitle' => 'Zamierzasz dodać do ulubionych:',
|
'subtitle' => 'Zamierzasz dodać do ulubionych:',
|
||||||
'submit' => 'Przejdź do dodania do ulubionych',
|
'submit' => 'Dodaj do ulubionych',
|
||||||
],
|
],
|
||||||
'reblog' => [
|
'reblog' => [
|
||||||
'title' => "Udostępnij wpis {actorDisplayName}",
|
'title' => "Udostępnij wpis {actorDisplayName}",
|
||||||
'subtitle' => 'Zamierzasz udostępnić:',
|
'subtitle' => 'Zamierzasz udostępnić:',
|
||||||
'submit' => 'Przejdź do udostępnienia',
|
'submit' => 'Udostępnij',
|
||||||
],
|
],
|
||||||
'reply' => [
|
'reply' => [
|
||||||
'title' => "Odpowiedź do wpisu {actorDisplayName}",
|
'title' => "Odpowiedz do wpisu {actorDisplayName}",
|
||||||
'subtitle' => 'Zamierzasz odpisać na:',
|
'subtitle' => 'Zamierzasz odpisać na:',
|
||||||
'submit' => 'Przejdź do odpowiedzi',
|
'submit' => 'Odpowiedz',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ declare(strict_types=1);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'back_to_home' => 'Wróć do początku',
|
'back_to_home' => 'Wróć do strony głównej',
|
||||||
'map' => [
|
'map' => [
|
||||||
'title' => 'Mapa',
|
'title' => 'Mapa',
|
||||||
'description' => 'Odkryj odcinki podcastów w witrynie {siteName} umieszczone na mapie! Podróżuj po mapie i słuchaj odcinków, które opowiadają o konkretnych lokalizacjach.',
|
'description' => 'Odkryj odcinki podcastów w witrynie {siteName}, które są umieszczone na mapie! Podróżuj po mapie i słuchaj odcinków, które opowiadają o konkretnych lokalizacjach.',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ return [
|
||||||
'Sezon {seasonNumber} odcinki ({episodeCount})',
|
'Sezon {seasonNumber} odcinki ({episodeCount})',
|
||||||
'no_episode' => 'Nie znaleziono odcinków!',
|
'no_episode' => 'Nie znaleziono odcinków!',
|
||||||
'follow' => 'Obserwuj',
|
'follow' => 'Obserwuj',
|
||||||
'followTitle' => 'Obserwuj {actorDisplayName} na fediverse!',
|
'followTitle' => 'Obserwuj {actorDisplayName} na fediwersum!',
|
||||||
'followers' => '{numberOfFollowers, plural,
|
'followers' => '{numberOfFollowers, plural,
|
||||||
one {# polubienie}
|
one {# polubienie}
|
||||||
few {# polubienia}
|
few {# polubienia}
|
||||||
|
|
@ -27,7 +27,7 @@ return [
|
||||||
few {# osoby}
|
few {# osoby}
|
||||||
other {# osób}
|
other {# osób}
|
||||||
}',
|
}',
|
||||||
'links' => 'Links',
|
'links' => 'Linki',
|
||||||
'activity' => 'Wpisy',
|
'activity' => 'Wpisy',
|
||||||
'episodes' => 'Odcinki',
|
'episodes' => 'Odcinki',
|
||||||
'episodes_title' => 'Odcinki {podcastTitle}',
|
'episodes_title' => 'Odcinki {podcastTitle}',
|
||||||
|
|
@ -56,5 +56,5 @@ return [
|
||||||
other {# osób}
|
other {# osób}
|
||||||
}',
|
}',
|
||||||
'persons_list' => 'Osoby',
|
'persons_list' => 'Osoby',
|
||||||
'castopod_website' => 'Castopod (website)',
|
'castopod_website' => 'Castopod (strona)',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,18 @@ class Breadcrumb
|
||||||
$uri = '';
|
$uri = '';
|
||||||
foreach (current_url(true)->getSegments() as $segment) {
|
foreach (current_url(true)->getSegments() as $segment) {
|
||||||
$uri .= '/' . $segment;
|
$uri .= '/' . $segment;
|
||||||
$this->links[] = [
|
$link = [
|
||||||
'text' => is_numeric($segment)
|
'text' => is_numeric($segment)
|
||||||
? $segment
|
? $segment
|
||||||
: lang('Breadcrumb.' . $segment),
|
: lang('Breadcrumb.' . $segment),
|
||||||
'href' => base_url($uri),
|
'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
|
* 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
|
public function replaceParams(array $newParams): void
|
||||||
{
|
{
|
||||||
foreach ($this->links as $key => $link) {
|
foreach ($newParams as $key => $newValue) {
|
||||||
if (is_numeric($link['text'])) {
|
if (array_key_exists($key, $this->links)) {
|
||||||
$this->links[$key]['text'] = $newParams[0];
|
$this->links[$key]['text'] = $newValue;
|
||||||
array_shift($newParams);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +72,7 @@ class Breadcrumb
|
||||||
/**
|
/**
|
||||||
* Renders the breadcrumb object as an accessible html breadcrumb nav
|
* 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 = '';
|
$listItems = '';
|
||||||
$keys = array_keys($this->links);
|
$keys = array_keys($this->links);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class CommentObject extends ObjectType
|
||||||
'episode-comment-replies',
|
'episode-comment-replies',
|
||||||
esc($comment->actor->username),
|
esc($comment->actor->username),
|
||||||
$comment->episode->slug,
|
$comment->episode->slug,
|
||||||
$comment->id
|
$comment->id,
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->cc = [$comment->actor->followers_url];
|
$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,10 +15,11 @@ declare(strict_types=1);
|
||||||
namespace App\Libraries;
|
namespace App\Libraries;
|
||||||
|
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\Router\Exceptions\RedirectException;
|
use CodeIgniter\HTTP\Exceptions\RedirectException;
|
||||||
use CodeIgniter\Router\Exceptions\RouterException;
|
use CodeIgniter\Router\Exceptions\RouterException;
|
||||||
use CodeIgniter\Router\Router as CodeIgniterRouter;
|
use CodeIgniter\Router\Router as CodeIgniterRouter;
|
||||||
use Config\Services;
|
use Config\Routing;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class Router extends CodeIgniterRouter
|
class Router extends CodeIgniterRouter
|
||||||
{
|
{
|
||||||
|
|
@ -30,6 +31,7 @@ class Router extends CodeIgniterRouter
|
||||||
*
|
*
|
||||||
* @return boolean Whether the route was matched or not.
|
* @return boolean Whether the route was matched or not.
|
||||||
*/
|
*/
|
||||||
|
#[Override]
|
||||||
protected function checkRoutes(string $uri): bool
|
protected function checkRoutes(string $uri): bool
|
||||||
{
|
{
|
||||||
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
|
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
|
||||||
|
|
@ -43,7 +45,7 @@ class Router extends CodeIgniterRouter
|
||||||
|
|
||||||
// Loop through the route array looking for wildcards
|
// Loop through the route array looking for wildcards
|
||||||
foreach ($routes as $routeKey => $handler) {
|
foreach ($routes as $routeKey => $handler) {
|
||||||
$routeKey = $routeKey === '/' ? $routeKey : ltrim($routeKey, '/ ');
|
$routeKey = $routeKey === '/' ? $routeKey : ltrim((string) $routeKey, '/ ');
|
||||||
|
|
||||||
$matchedKey = $routeKey;
|
$matchedKey = $routeKey;
|
||||||
|
|
||||||
|
|
@ -67,7 +69,7 @@ class Router extends CodeIgniterRouter
|
||||||
|
|
||||||
throw new RedirectException(
|
throw new RedirectException(
|
||||||
preg_replace('#^' . $routeKey . '$#u', (string) $redirectTo, $uri),
|
preg_replace('#^' . $routeKey . '$#u', (string) $redirectTo, $uri),
|
||||||
$this->collection->getRedirectCode($routeKey)
|
$this->collection->getRedirectCode($routeKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +79,7 @@ class Router extends CodeIgniterRouter
|
||||||
preg_match(
|
preg_match(
|
||||||
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
|
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
|
||||||
$uri,
|
$uri,
|
||||||
$matched
|
$matched,
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->collection->shouldUseSupportedLocalesOnly()
|
if ($this->collection->shouldUseSupportedLocalesOnly()
|
||||||
|
|
@ -115,8 +117,8 @@ class Router extends CodeIgniterRouter
|
||||||
array_key_exists('alternate-content', $this->matchedRouteOptions) &&
|
array_key_exists('alternate-content', $this->matchedRouteOptions) &&
|
||||||
is_array($this->matchedRouteOptions['alternate-content'])
|
is_array($this->matchedRouteOptions['alternate-content'])
|
||||||
) {
|
) {
|
||||||
$request = Services::request();
|
$request = service('request');
|
||||||
$negotiate = Services::negotiator();
|
$negotiate = service('negotiator');
|
||||||
|
|
||||||
// Accept header is mandatory
|
// Accept header is mandatory
|
||||||
if ($request->header('Accept') === null) {
|
if ($request->header('Accept') === null) {
|
||||||
|
|
@ -180,24 +182,50 @@ class Router extends CodeIgniterRouter
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[$controller] = explode('::', (string) $handler);
|
if (str_contains((string) $handler, '::')) {
|
||||||
|
[$controller, $methodAndParams] = explode('::', (string) $handler);
|
||||||
|
} else {
|
||||||
|
$controller = $handler;
|
||||||
|
$methodAndParams = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Checks `/` in controller name
|
// Checks `/` in controller name
|
||||||
if (str_contains($controller, '/')) {
|
if (str_contains((string) $controller, '/')) {
|
||||||
throw RouterException::forInvalidControllerName($handler);
|
throw RouterException::forInvalidControllerName($handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
|
if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
|
||||||
// Checks dynamic controller
|
// Checks dynamic controller
|
||||||
if (str_contains($controller, '$')) {
|
if (str_contains((string) $controller, '$')) {
|
||||||
throw RouterException::forDynamicController($handler);
|
throw RouterException::forDynamicController($handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using back-references
|
if (config(Routing::class)->multipleSegmentsOneParam === false) {
|
||||||
$handler = preg_replace('#^' . $routeKey . '$#u', (string) $handler, $uri);
|
// 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);
|
$this->setMatchedRoute($matchedKey, $handler);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,34 @@ declare(strict_types=1);
|
||||||
namespace App\Libraries;
|
namespace App\Libraries;
|
||||||
|
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
|
use Override;
|
||||||
use SimpleXMLElement;
|
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
|
* 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.
|
* @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
|
public function addChild($name, $value = null, $namespace = null, $escape = true): static
|
||||||
{
|
{
|
||||||
$newChild = parent::addChild($name, null, $namespace);
|
$newChild = parent::addChild($name, null, $namespace);
|
||||||
|
|
@ -57,12 +82,41 @@ class SimpleRSSElement extends SimpleXMLElement
|
||||||
return $newChild;
|
return $newChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($value)) {
|
|
||||||
return $newChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
$node->appendChild($no->createTextNode($value));
|
$node->appendChild($no->createTextNode($value));
|
||||||
|
|
||||||
return $newChild;
|
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;
|
namespace ViewComponents;
|
||||||
|
|
||||||
class Component implements ComponentInterface
|
use Override;
|
||||||
{
|
|
||||||
protected string $slot = '';
|
|
||||||
|
|
||||||
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>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
protected array $attributes = [
|
protected array $attributes = [];
|
||||||
'class' => '',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, string> $attributes
|
* @param array<string, string> $attributes
|
||||||
*/
|
*/
|
||||||
public function __construct(array $attributes)
|
public function __construct(array $attributes)
|
||||||
{
|
{
|
||||||
helper('viewcomponents');
|
|
||||||
|
|
||||||
// overwrite default attributes if set
|
// overwrite default attributes if set
|
||||||
$this->attributes = [...$this->attributes, ...$attributes];
|
$this->attributes = [...$this->attributes, ...$attributes];
|
||||||
|
|
||||||
|
|
@ -42,11 +48,42 @@ class Component implements ComponentInterface
|
||||||
if (is_callable([$this, $method])) {
|
if (is_callable([$this, $method])) {
|
||||||
$this->{$method}($value);
|
$this->{$method}($value);
|
||||||
} else {
|
} 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;
|
$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
|
public function render(): string
|
||||||
{
|
{
|
||||||
return static::class . ': RENDER METHOD NOT IMPLEMENTED';
|
return static::class . ': RENDER METHOD NOT IMPLEMENTED';
|
||||||
|
|
|
||||||
|
|
@ -43,38 +43,38 @@ class ComponentRenderer
|
||||||
private function renderSelfClosingTags(string $output): string
|
private function renderSelfClosingTags(string $output): string
|
||||||
{
|
{
|
||||||
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
|
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
|
||||||
// Should match any Component tags <Component />
|
// Should match any Component tags <x-Component />
|
||||||
$pattern = "/
|
$pattern = "/
|
||||||
<
|
<
|
||||||
\s*
|
\\s*
|
||||||
(?<name>[A-Z][A-Za-z0-9\.]*?)
|
x[-\\:](?<name>[\\w\\-\\:\\.]*)
|
||||||
\s*
|
\\s*
|
||||||
(?<attributes>
|
(?<attributes>
|
||||||
(?:
|
(?:
|
||||||
\s+
|
\\s+
|
||||||
(?:
|
(?:
|
||||||
(?:
|
(?:
|
||||||
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
|
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
|
||||||
)
|
)
|
||||||
|
|
|
|
||||||
(?:
|
(?:
|
||||||
[\w\-:.@]+
|
[\\w\\-:.@]+
|
||||||
(
|
(
|
||||||
=
|
=
|
||||||
(?:
|
(?:
|
||||||
\\\"[^\\\"]*\\\"
|
\\\"[^\\\"]*\\\"
|
||||||
|
|
|
|
||||||
\'[^\']*\'
|
\\'[^\\']*\\'
|
||||||
|
|
|
|
||||||
[^\'\\\"=<>]+
|
[^\\'\\\"=<>]+
|
||||||
)
|
)
|
||||||
)?
|
)?
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)*
|
)*
|
||||||
\s*
|
\\s*
|
||||||
)
|
)
|
||||||
\/>
|
\\/>
|
||||||
/x";
|
/x";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -96,8 +96,9 @@ class ComponentRenderer
|
||||||
|
|
||||||
private function renderPairedTags(string $output): string
|
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[0] = full tags matched and all of its content
|
||||||
$matches[name] = pascal cased tag name
|
$matches[name] = pascal cased tag name
|
||||||
|
|
@ -167,8 +168,6 @@ class ComponentRenderer
|
||||||
(
|
(
|
||||||
\"[^\"]+\"
|
\"[^\"]+\"
|
||||||
|
|
|
|
||||||
\'[^\']+\'
|
|
||||||
|
|
|
||||||
\\\'[^\\\']+\\\'
|
\\\'[^\\\']+\\\'
|
||||||
|
|
|
|
||||||
[^\s>]+
|
[^\s>]+
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class Services extends BaseService
|
||||||
public static function components(bool $getShared = true): ComponentRenderer
|
public static function components(bool $getShared = true): ComponentRenderer
|
||||||
{
|
{
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
|
/** @phpstan-ignore return.type */
|
||||||
return self::getSharedInstance('components');
|
return self::getSharedInstance('components');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace ViewComponents;
|
namespace ViewComponents;
|
||||||
|
|
||||||
use CodeIgniter\View\ViewDecoratorInterface;
|
use CodeIgniter\View\ViewDecoratorInterface;
|
||||||
|
use Override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables rendering of View Components into the views.
|
* Enables rendering of View Components into the views.
|
||||||
|
|
@ -15,6 +16,7 @@ class Decorator implements ViewDecoratorInterface
|
||||||
{
|
{
|
||||||
private static ?ComponentRenderer $components = null;
|
private static ?ComponentRenderer $components = null;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public static function decorate(string $html): string
|
public static function decorate(string $html): string
|
||||||
{
|
{
|
||||||
$components = self::factory();
|
$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.
|
* 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) {
|
if ($theme === null) {
|
||||||
$theme = static::current();
|
$theme = static::current();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Vite\Config;
|
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseService;
|
|
||||||
use Vite\Vite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Services Configuration file.
|
|
||||||
*
|
|
||||||
* Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow
|
|
||||||
* the core of the framework to be swapped out easily without affecting the usage within the rest of your application.
|
|
||||||
*
|
|
||||||
* This file holds any application-specific services, or service overrides that you might need. An example has been
|
|
||||||
* included with the general method format you should use for your service methods. For more examples, see the core
|
|
||||||
* Services file at system/Config/Services.php.
|
|
||||||
*/
|
|
||||||
class Services extends BaseService
|
|
||||||
{
|
|
||||||
public static function vite(bool $getShared = true): Vite
|
|
||||||
{
|
|
||||||
if ($getShared) {
|
|
||||||
return self::getSharedInstance('vite');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Vite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Vite\Config;
|
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
|
||||||
|
|
||||||
class Vite extends BaseConfig
|
|
||||||
{
|
|
||||||
public string $environment = 'production';
|
|
||||||
|
|
||||||
public string $baseUrl = 'http://localhost:5173/';
|
|
||||||
|
|
||||||
public string $assetsRoot = 'assets';
|
|
||||||
|
|
||||||
public string $manifestFile = '.vite/manifest.json';
|
|
||||||
|
|
||||||
public string $manifestCSSFile = 'manifest-css.json';
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Vite;
|
|
||||||
|
|
||||||
use ErrorException;
|
|
||||||
|
|
||||||
class Vite
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
protected ?array $manifestData = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
protected ?array $manifestCSSData = null;
|
|
||||||
|
|
||||||
public function asset(string $path, string $type): string
|
|
||||||
{
|
|
||||||
if (config('Vite')->environment !== 'production') {
|
|
||||||
return $this->loadDev($path, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->loadProd($path, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadDev(string $path, string $type): string
|
|
||||||
{
|
|
||||||
return $this->getHtmlTag(config('Vite') ->baseUrl . config('Vite')->assetsRoot . "/{$path}", $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadProd(string $path, string $type): string
|
|
||||||
{
|
|
||||||
if ($this->manifestData === null) {
|
|
||||||
$cacheName = 'vite-manifest';
|
|
||||||
if (! ($cachedManifest = cache($cacheName))) {
|
|
||||||
$manifestPath = config('Vite')
|
|
||||||
->assetsRoot . '/' . config('Vite')
|
|
||||||
->manifestFile;
|
|
||||||
try {
|
|
||||||
if (($manifestContents = file_get_contents($manifestPath)) !== false) {
|
|
||||||
$cachedManifest = json_decode($manifestContents, true);
|
|
||||||
cache()
|
|
||||||
->save($cacheName, $cachedManifest, DECADE);
|
|
||||||
}
|
|
||||||
} catch (ErrorException) {
|
|
||||||
// ERROR when retrieving the manifest file
|
|
||||||
die("Could not load manifest: <strong>{$manifestPath}</strong> file not found!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->manifestData = $cachedManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = '';
|
|
||||||
if (array_key_exists($path, $this->manifestData)) {
|
|
||||||
$manifestElement = $this->manifestData[$path];
|
|
||||||
|
|
||||||
// import css dependencies if any
|
|
||||||
if (array_key_exists('css', $manifestElement)) {
|
|
||||||
foreach ($manifestElement['css'] as $cssFile) {
|
|
||||||
$html .= $this->getHtmlTag('/' . config('Vite')->assetsRoot . '/' . $cssFile, 'css');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// import dependencies first for faster js loading
|
|
||||||
if (array_key_exists('imports', $manifestElement)) {
|
|
||||||
foreach ($manifestElement['imports'] as $importPath) {
|
|
||||||
if (array_key_exists($importPath, $this->manifestData)) {
|
|
||||||
// import css dependencies if any
|
|
||||||
if (array_key_exists('css', $this->manifestData[$importPath])) {
|
|
||||||
foreach ($this->manifestData[$importPath]['css'] as $cssFile) {
|
|
||||||
$html .= $this->getHtmlTag(
|
|
||||||
'/' . config('Vite')->assetsRoot . '/' . $cssFile,
|
|
||||||
'css'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$html .= $this->getHtmlTag(
|
|
||||||
'/' . config('Vite')->assetsRoot . '/' . $this->manifestData[$importPath]['file'],
|
|
||||||
'js'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$html .= $this->getHtmlTag('/' . config('Vite')->assetsRoot . '/' . $manifestElement['file'], $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getHtmlTag(string $assetUrl, string $type): string
|
|
||||||
{
|
|
||||||
return match ($type) {
|
|
||||||
'css' => <<<HTML
|
|
||||||
<link rel="stylesheet" href="{$assetUrl}"/>
|
|
||||||
HTML
|
|
||||||
,
|
|
||||||
'js' => <<<HTML
|
|
||||||
<script type="module" src="{$assetUrl}"></script>
|
|
||||||
HTML
|
|
||||||
,
|
|
||||||
default => '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,14 +12,16 @@ namespace App\Models;
|
||||||
|
|
||||||
use App\Entities\Actor;
|
use App\Entities\Actor;
|
||||||
use Modules\Fediverse\Models\ActorModel as FediverseActorModel;
|
use Modules\Fediverse\Models\ActorModel as FediverseActorModel;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class ActorModel extends FediverseActorModel
|
class ActorModel extends FediverseActorModel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Actor>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Actor::class;
|
protected $returnType = Actor::class;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function getActorById(int $id): ?Actor
|
public function getActorById(int $id): ?Actor
|
||||||
{
|
{
|
||||||
return $this->find($id);
|
return $this->find($id);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class CategoryModel extends Model
|
||||||
protected $allowedFields = ['parent_id', 'code', 'apple_category', 'google_category'];
|
protected $allowedFields = ['parent_id', 'code', 'apple_category', 'google_category'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Category>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Category::class;
|
protected $returnType = Category::class;
|
||||||
|
|
||||||
|
|
@ -65,12 +65,17 @@ class CategoryModel extends Model
|
||||||
$options = array_reduce(
|
$options = array_reduce(
|
||||||
$categories,
|
$categories,
|
||||||
static function (array $result, Category $category): array {
|
static function (array $result, Category $category): array {
|
||||||
$result[$category->id] = '';
|
$label = '';
|
||||||
if ($category->parent instanceof Category) {
|
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;
|
return $result;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,8 @@ class ClipModel extends Model
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $type = 'audio',
|
protected string $type = 'audio',
|
||||||
ConnectionInterface &$db = null,
|
?ConnectionInterface &$db = null,
|
||||||
ValidationInterface $validation = null
|
?ValidationInterface $validation = null,
|
||||||
) {
|
) {
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'audio':
|
case 'audio':
|
||||||
|
|
@ -122,7 +122,6 @@ class ClipModel extends Model
|
||||||
$found[$key] = new VideoClip($videoClip->toArray());
|
$found[$key] = new VideoClip($videoClip->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
return $found;
|
return $found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,15 +161,11 @@ class ClipModel extends Model
|
||||||
return (int) $result[0]['id'];
|
return (int) $result[0]['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteVideoClip(int $podcastId, int $episodeId, int $clipId): BaseResult | bool
|
public function deleteVideoClip(int $clipId): BaseResult | bool
|
||||||
{
|
{
|
||||||
$this->clearVideoClipCache($clipId);
|
$this->clearVideoClipCache($clipId);
|
||||||
|
|
||||||
return $this->delete([
|
return $this->delete($clipId);
|
||||||
'podcast_id' => $podcastId,
|
|
||||||
'episode_id' => $episodeId,
|
|
||||||
'id' => $clipId,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClipCount(int $podcastId, int $episodeId): int
|
public function getClipCount(int $podcastId, int $episodeId): int
|
||||||
|
|
@ -240,11 +235,7 @@ class ClipModel extends Model
|
||||||
{
|
{
|
||||||
$this->clearSoundbiteCache($podcastId, $episodeId, $clipId);
|
$this->clearSoundbiteCache($podcastId, $episodeId, $clipId);
|
||||||
|
|
||||||
return $this->delete([
|
return $this->delete($clipId);
|
||||||
'podcast_id' => $podcastId,
|
|
||||||
'episode_id' => $episodeId,
|
|
||||||
'id' => $clipId,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearSoundbiteCache(int $podcastId, int $episodeId, int $clipId): void
|
public function clearSoundbiteCache(int $podcastId, int $episodeId, int $clipId): void
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class CreditModel extends Model
|
||||||
protected $table = 'credits';
|
protected $table = 'credits';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Credit>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Credit::class;
|
protected $returnType = Credit::class;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use Modules\Fediverse\Objects\TombstoneObject;
|
||||||
class EpisodeCommentModel extends UuidModel
|
class EpisodeCommentModel extends UuidModel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<EpisodeComment>
|
||||||
*/
|
*/
|
||||||
protected $returnType = EpisodeComment::class;
|
protected $returnType = EpisodeComment::class;
|
||||||
|
|
||||||
|
|
@ -86,11 +86,13 @@ class EpisodeCommentModel extends UuidModel
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($comment->in_reply_to_id === null) {
|
if ($comment->in_reply_to_id === null) {
|
||||||
(new EpisodeModel())->builder()
|
new EpisodeModel()
|
||||||
|
->builder()
|
||||||
->where('id', $comment->episode_id)
|
->where('id', $comment->episode_id)
|
||||||
->increment('comments_count');
|
->increment('comments_count');
|
||||||
} else {
|
} else {
|
||||||
(new self())->builder()
|
new self()
|
||||||
|
->builder()
|
||||||
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
||||||
->increment('replies_count');
|
->increment('replies_count');
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +104,7 @@ class EpisodeCommentModel extends UuidModel
|
||||||
'episode-comment',
|
'episode-comment',
|
||||||
esc($comment->actor->username),
|
esc($comment->actor->username),
|
||||||
$comment->episode->slug,
|
$comment->episode->slug,
|
||||||
$comment->id
|
$comment->id,
|
||||||
);
|
);
|
||||||
|
|
||||||
$createActivity = new CreateActivity();
|
$createActivity = new CreateActivity();
|
||||||
|
|
@ -180,7 +182,8 @@ class EpisodeCommentModel extends UuidModel
|
||||||
->where('id', $comment->episode_id)
|
->where('id', $comment->episode_id)
|
||||||
->decrement('comments_count');
|
->decrement('comments_count');
|
||||||
} else {
|
} else {
|
||||||
(new self())->builder()
|
new self()
|
||||||
|
->builder()
|
||||||
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
|
||||||
->decrement('replies_count');
|
->decrement('replies_count');
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +204,7 @@ class EpisodeCommentModel extends UuidModel
|
||||||
{
|
{
|
||||||
// TODO: merge with replies from posts linked to episode linked
|
// TODO: merge with replies from posts linked to episode linked
|
||||||
$episodeCommentsBuilder = $this->builder();
|
$episodeCommentsBuilder = $this->builder();
|
||||||
$episodeComments = $episodeCommentsBuilder->select('*, 0 as is_from_post')
|
$episodeComments = $episodeCommentsBuilder->select('*, 0 as is_private, 0 as is_from_post')
|
||||||
->where([
|
->where([
|
||||||
'episode_id' => $episodeId,
|
'episode_id' => $episodeId,
|
||||||
'in_reply_to_id' => null,
|
'in_reply_to_id' => null,
|
||||||
|
|
@ -211,7 +214,7 @@ class EpisodeCommentModel extends UuidModel
|
||||||
$postModel = new PostModel();
|
$postModel = new PostModel();
|
||||||
$episodePostsRepliesBuilder = $postModel->builder();
|
$episodePostsRepliesBuilder = $postModel->builder();
|
||||||
$episodePostsReplies = $episodePostsRepliesBuilder->select(
|
$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, 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 {
|
->whereIn('in_reply_to_id', static function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
|
||||||
return $builder->select('id')
|
return $builder->select('id')
|
||||||
|
|
@ -221,19 +224,23 @@ class EpisodeCommentModel extends UuidModel
|
||||||
'in_reply_to_id' => null,
|
'in_reply_to_id' => null,
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
->where('`created_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`created_at` <= UTC_TIMESTAMP()', null, false);
|
||||||
->getCompiledSelect();
|
|
||||||
|
// do not get private replies if public
|
||||||
|
if (! can_user_interact()) {
|
||||||
|
$episodePostsRepliesBuilder->where('is_private', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodePostsReplies = $episodePostsRepliesBuilder->getCompiledSelect();
|
||||||
|
|
||||||
/** @var BaseResult $allEpisodeComments */
|
/** @var BaseResult $allEpisodeComments */
|
||||||
$allEpisodeComments = $this->db->query(
|
$allEpisodeComments = $this->db->query(
|
||||||
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
|
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC',
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME:?
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
return $this->convertUuidFieldsToStrings(
|
return $this->convertUuidFieldsToStrings(
|
||||||
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
|
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
|
||||||
$this->tempReturnType
|
$this->tempReturnType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ class EpisodeModel extends UuidModel
|
||||||
'location_name',
|
'location_name',
|
||||||
'location_geo',
|
'location_geo',
|
||||||
'location_osm',
|
'location_osm',
|
||||||
'custom_rss',
|
|
||||||
'is_published_on_hubs',
|
'is_published_on_hubs',
|
||||||
|
'downloads_count',
|
||||||
'posts_count',
|
'posts_count',
|
||||||
'comments_count',
|
'comments_count',
|
||||||
'is_premium',
|
'is_premium',
|
||||||
|
|
@ -98,7 +98,7 @@ class EpisodeModel extends UuidModel
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Episode>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Episode::class;
|
protected $returnType = Episode::class;
|
||||||
|
|
||||||
|
|
@ -197,7 +197,7 @@ class EpisodeModel extends UuidModel
|
||||||
|
|
||||||
public function getEpisodeByPreviewId(string $previewId): ?Episode
|
public function getEpisodeByPreviewId(string $previewId): ?Episode
|
||||||
{
|
{
|
||||||
$cacheName = "podcast_episode#preview-{$previewId}";
|
$cacheName = "podcast_episode-preview#{$previewId}";
|
||||||
if (! ($found = cache($cacheName))) {
|
if (! ($found = cache($cacheName))) {
|
||||||
$builder = $this->where([
|
$builder = $this->where([
|
||||||
'preview_id' => $this->uuid->fromString($previewId)
|
'preview_id' => $this->uuid->fromString($previewId)
|
||||||
|
|
@ -235,8 +235,8 @@ class EpisodeModel extends UuidModel
|
||||||
public function getPodcastEpisodes(
|
public function getPodcastEpisodes(
|
||||||
int $podcastId,
|
int $podcastId,
|
||||||
string $podcastType,
|
string $podcastType,
|
||||||
string $year = null,
|
?string $year = null,
|
||||||
string $season = null
|
?string $season = null,
|
||||||
): array {
|
): array {
|
||||||
$cacheName = implode(
|
$cacheName = implode(
|
||||||
'_',
|
'_',
|
||||||
|
|
@ -347,7 +347,7 @@ class EpisodeModel extends UuidModel
|
||||||
{
|
{
|
||||||
$result = $this->builder()
|
$result = $this->builder()
|
||||||
->select(
|
->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('podcast_id', $podcastId)
|
||||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||||
|
|
@ -368,29 +368,32 @@ class EpisodeModel extends UuidModel
|
||||||
|
|
||||||
public function resetCommentsCount(): int | false
|
public function resetCommentsCount(): int | false
|
||||||
{
|
{
|
||||||
$episodeCommentsCount = (new EpisodeCommentModel())->builder()
|
$episodeCommentsCount = new EpisodeCommentModel()
|
||||||
|
->builder()
|
||||||
->select('episode_id, COUNT(*) as `comments_count`')
|
->select('episode_id, COUNT(*) as `comments_count`')
|
||||||
->where('in_reply_to_id', null)
|
->where('in_reply_to_id')
|
||||||
->groupBy('episode_id')
|
->groupBy('episode_id')
|
||||||
->getCompiledSelect();
|
->getCompiledSelect();
|
||||||
|
|
||||||
$episodePostsRepliesCount = (new PostModel())->builder()
|
$episodePostsRepliesCount = new PostModel()
|
||||||
|
->builder()
|
||||||
->select('fediverse_posts.episode_id as episode_id, COUNT(*) as `comments_count`')
|
->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')
|
->join('fediverse_posts as fp', 'fediverse_posts.id = fp.in_reply_to_id')
|
||||||
->where('fediverse_posts.in_reply_to_id', null)
|
->where('fediverse_posts.in_reply_to_id')
|
||||||
->where('fediverse_posts.episode_id IS NOT', null)
|
->where('fediverse_posts.episode_id IS NOT')
|
||||||
->groupBy('fediverse_posts.episode_id')
|
->groupBy('fediverse_posts.episode_id')
|
||||||
->getCompiledSelect();
|
->getCompiledSelect();
|
||||||
|
|
||||||
/** @var BaseResult $query */
|
/** @var BaseResult $query */
|
||||||
$query = $this->db->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();
|
$countsPerEpisodeId = $query->getResultArray();
|
||||||
|
|
||||||
if ($countsPerEpisodeId !== []) {
|
if ($countsPerEpisodeId !== []) {
|
||||||
return (new self())->updateBatch($countsPerEpisodeId, 'id');
|
return new self()
|
||||||
|
->updateBatch($countsPerEpisodeId, 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -401,7 +404,7 @@ class EpisodeModel extends UuidModel
|
||||||
$episodePostsCount = $this->builder()
|
$episodePostsCount = $this->builder()
|
||||||
->select('episodes.id, COUNT(*) as `posts_count`')
|
->select('episodes.id, COUNT(*) as `posts_count`')
|
||||||
->join('fediverse_posts', 'episodes.id = fediverse_posts.episode_id')
|
->join('fediverse_posts', 'episodes.id = fediverse_posts.episode_id')
|
||||||
->where('in_reply_to_id', null)
|
->where('in_reply_to_id')
|
||||||
->groupBy('episodes.id')
|
->groupBy('episodes.id')
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|
@ -429,7 +432,8 @@ class EpisodeModel extends UuidModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var ?Episode $episode */
|
/** @var ?Episode $episode */
|
||||||
$episode = (new self())->find($episodeId);
|
$episode = new self()
|
||||||
|
->find($episodeId);
|
||||||
|
|
||||||
if (! $episode instanceof Episode) {
|
if (! $episode instanceof Episode) {
|
||||||
return $data;
|
return $data;
|
||||||
|
|
@ -441,7 +445,7 @@ class EpisodeModel extends UuidModel
|
||||||
cache()
|
cache()
|
||||||
->deleteMatching("podcast-{$episode->podcast->handle}*");
|
->deleteMatching("podcast-{$episode->podcast->handle}*");
|
||||||
cache()
|
cache()
|
||||||
->delete("podcast_episode#{$episode->id}");
|
->deleteMatching('podcast_episode*');
|
||||||
cache()
|
cache()
|
||||||
->deleteMatching("page_podcast#{$episode->podcast_id}*");
|
->deleteMatching("page_podcast#{$episode->podcast_id}*");
|
||||||
cache()
|
cache()
|
||||||
|
|
@ -480,7 +484,7 @@ class EpisodeModel extends UuidModel
|
||||||
')
|
')
|
||||||
->select("{$podcastTable}.created_at AS podcast_created_at")
|
->select("{$podcastTable}.created_at AS podcast_created_at")
|
||||||
->select(
|
->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")
|
->join($podcastTable, "{$podcastTable} on {$podcastTable}.id = {$episodeTable}.podcast_id")
|
||||||
->where('
|
->where('
|
||||||
|
|
@ -489,7 +493,7 @@ class EpisodeModel extends UuidModel
|
||||||
. 'OR' .
|
. 'OR' .
|
||||||
$podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
|
$podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
|
||||||
. ')
|
. ')
|
||||||
');
|
', );
|
||||||
|
|
||||||
return $this->builder;
|
return $this->builder;
|
||||||
}
|
}
|
||||||
|
|
@ -523,7 +527,8 @@ class EpisodeModel extends UuidModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var ?Episode $episode */
|
/** @var ?Episode $episode */
|
||||||
$episode = (new self())->find($episodeId);
|
$episode = new self()
|
||||||
|
->find($episodeId);
|
||||||
|
|
||||||
if (! $episode instanceof Episode) {
|
if (! $episode instanceof Episode) {
|
||||||
return $data;
|
return $data;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class LanguageModel extends Model
|
||||||
protected $allowedFields = ['code', 'native_name'];
|
protected $allowedFields = ['code', 'native_name'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Language>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Language::class;
|
protected $returnType = Language::class;
|
||||||
|
|
||||||
|
|
@ -56,7 +56,10 @@ class LanguageModel extends Model
|
||||||
$options = array_reduce(
|
$options = array_reduce(
|
||||||
$languages,
|
$languages,
|
||||||
static function (array $result, Language $language): array {
|
static function (array $result, Language $language): array {
|
||||||
$result[$language->code] = $language->native_name;
|
$result[] = [
|
||||||
|
'value' => $language->code,
|
||||||
|
'label' => $language->native_name,
|
||||||
|
];
|
||||||
return $result;
|
return $result;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class LikeModel extends UuidModel
|
||||||
protected $allowedFields = ['actor_id', 'comment_id'];
|
protected $allowedFields = ['actor_id', 'comment_id'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Like>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Like::class;
|
protected $returnType = Like::class;
|
||||||
|
|
||||||
|
|
@ -56,7 +56,8 @@ class LikeModel extends UuidModel
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new EpisodeCommentModel())->builder()
|
new EpisodeCommentModel()
|
||||||
|
->builder()
|
||||||
->where('id', service('uuid')->fromString($comment->id)->getBytes())
|
->where('id', service('uuid')->fromString($comment->id)->getBytes())
|
||||||
->increment('likes_count');
|
->increment('likes_count');
|
||||||
|
|
||||||
|
|
@ -91,7 +92,8 @@ class LikeModel extends UuidModel
|
||||||
{
|
{
|
||||||
$this->db->transStart();
|
$this->db->transStart();
|
||||||
|
|
||||||
(new EpisodeCommentModel())->builder()
|
new EpisodeCommentModel()
|
||||||
|
->builder()
|
||||||
->where('id', service('uuid') ->fromString($comment->id) ->getBytes())
|
->where('id', service('uuid') ->fromString($comment->id) ->getBytes())
|
||||||
->decrement('likes_count');
|
->decrement('likes_count');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class PageModel extends Model
|
||||||
protected $allowedFields = ['id', 'title', 'slug', 'content_markdown', 'content_html'];
|
protected $allowedFields = ['id', 'title', 'slug', 'content_markdown', 'content_html'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Page>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Page::class;
|
protected $returnType = Page::class;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class PersonModel extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Person>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Person::class;
|
protected $returnType = Person::class;
|
||||||
|
|
||||||
|
|
@ -145,8 +145,11 @@ class PersonModel extends Model
|
||||||
$this->select('`id`, `full_name`')
|
$this->select('`id`, `full_name`')
|
||||||
->orderBy('`full_name`', 'ASC')
|
->orderBy('`full_name`', 'ASC')
|
||||||
->findAll(),
|
->findAll(),
|
||||||
static function (array $result, $person): array {
|
static function (array $result, Person $person): array {
|
||||||
$result[$person->id] = $person->full_name;
|
$result[] = [
|
||||||
|
'value' => $person->id,
|
||||||
|
'label' => $person->full_name,
|
||||||
|
];
|
||||||
return $result;
|
return $result;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|
@ -174,9 +177,10 @@ class PersonModel extends Model
|
||||||
if (! ($options = cache($cacheName))) {
|
if (! ($options = cache($cacheName))) {
|
||||||
foreach ($personsTaxonomy as $group_key => $group) {
|
foreach ($personsTaxonomy as $group_key => $group) {
|
||||||
foreach ($group['roles'] as $role_key => $role) {
|
foreach ($group['roles'] as $role_key => $role) {
|
||||||
$options[
|
$options[] = [
|
||||||
"{$group_key},{$role_key}"
|
'value' => sprintf('%s,%s', $group_key, $role_key),
|
||||||
] = "{$group['label']} › {$role['label']}";
|
'label' => sprintf('%s › %s', $group['label'], $role['label']),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +215,7 @@ class PersonModel extends Model
|
||||||
if (! ($found = cache($cacheName))) {
|
if (! ($found = cache($cacheName))) {
|
||||||
$this->builder()
|
$this->builder()
|
||||||
->select(
|
->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()
|
->distinct()
|
||||||
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
|
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
|
||||||
|
|
@ -253,7 +257,7 @@ class PersonModel extends Model
|
||||||
int $episodeId,
|
int $episodeId,
|
||||||
int $personId,
|
int $personId,
|
||||||
string $groupSlug,
|
string $groupSlug,
|
||||||
string $roleSlug
|
string $roleSlug,
|
||||||
): bool {
|
): bool {
|
||||||
return $this->db->table('episodes_persons')
|
return $this->db->table('episodes_persons')
|
||||||
->insert([
|
->insert([
|
||||||
|
|
@ -293,9 +297,10 @@ class PersonModel extends Model
|
||||||
|
|
||||||
cache()
|
cache()
|
||||||
->delete("podcast#{$podcastId}_persons");
|
->delete("podcast#{$podcastId}_persons");
|
||||||
(new PodcastModel())->clearCache([
|
new PodcastModel()
|
||||||
'id' => $podcastId,
|
->clearCache([
|
||||||
]);
|
'id' => $podcastId,
|
||||||
|
]);
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($personIds as $personId) {
|
foreach ($personIds as $personId) {
|
||||||
|
|
@ -335,9 +340,10 @@ class PersonModel extends Model
|
||||||
cache()->deleteMatching("podcast#{$podcastId}_person#{$personId}*");
|
cache()->deleteMatching("podcast#{$podcastId}_person#{$personId}*");
|
||||||
cache()
|
cache()
|
||||||
->delete("podcast#{$podcastId}_persons");
|
->delete("podcast#{$podcastId}_persons");
|
||||||
(new PodcastModel())->clearCache([
|
new PodcastModel()
|
||||||
'id' => $podcastId,
|
->clearCache([
|
||||||
]);
|
'id' => $podcastId,
|
||||||
|
]);
|
||||||
|
|
||||||
return $this->db->table('podcasts_persons')
|
return $this->db->table('podcasts_persons')
|
||||||
->delete([
|
->delete([
|
||||||
|
|
@ -359,9 +365,10 @@ class PersonModel extends Model
|
||||||
if ($personIds !== []) {
|
if ($personIds !== []) {
|
||||||
cache()
|
cache()
|
||||||
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
||||||
(new EpisodeModel())->clearCache([
|
new EpisodeModel()
|
||||||
'id' => $episodeId,
|
->clearCache([
|
||||||
]);
|
'id' => $episodeId,
|
||||||
|
]);
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($personIds as $personId) {
|
foreach ($personIds as $personId) {
|
||||||
|
|
@ -400,9 +407,10 @@ class PersonModel extends Model
|
||||||
cache()->deleteMatching("podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}*");
|
cache()->deleteMatching("podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}*");
|
||||||
cache()
|
cache()
|
||||||
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
|
||||||
(new EpisodeModel())->clearCache([
|
new EpisodeModel()
|
||||||
'id' => $episodeId,
|
->clearCache([
|
||||||
]);
|
'id' => $episodeId,
|
||||||
|
]);
|
||||||
|
|
||||||
return $this->db->table('episodes_persons')
|
return $this->db->table('episodes_persons')
|
||||||
->delete([
|
->delete([
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ class PodcastModel extends Model
|
||||||
'handle',
|
'handle',
|
||||||
'description_markdown',
|
'description_markdown',
|
||||||
'description_html',
|
'description_html',
|
||||||
'episode_description_footer_markdown',
|
|
||||||
'episode_description_footer_html',
|
|
||||||
'cover_id',
|
'cover_id',
|
||||||
'banner_id',
|
'banner_id',
|
||||||
'language_code',
|
'language_code',
|
||||||
|
|
@ -47,10 +45,8 @@ class PodcastModel extends Model
|
||||||
'parental_advisory',
|
'parental_advisory',
|
||||||
'owner_name',
|
'owner_name',
|
||||||
'owner_email',
|
'owner_email',
|
||||||
'is_owner_email_removed_from_feed',
|
|
||||||
'publisher',
|
'publisher',
|
||||||
'type',
|
'type',
|
||||||
'medium',
|
|
||||||
'copyright',
|
'copyright',
|
||||||
'imported_feed_url',
|
'imported_feed_url',
|
||||||
'new_feed_url',
|
'new_feed_url',
|
||||||
|
|
@ -60,13 +56,7 @@ class PodcastModel extends Model
|
||||||
'location_name',
|
'location_name',
|
||||||
'location_geo',
|
'location_geo',
|
||||||
'location_osm',
|
'location_osm',
|
||||||
'verify_txt',
|
|
||||||
'payment_pointer',
|
|
||||||
'custom_rss',
|
|
||||||
'is_published_on_hubs',
|
'is_published_on_hubs',
|
||||||
'partner_id',
|
|
||||||
'partner_link_url',
|
|
||||||
'partner_image_url',
|
|
||||||
'is_premium_by_default',
|
'is_premium_by_default',
|
||||||
'published_at',
|
'published_at',
|
||||||
'created_by',
|
'created_by',
|
||||||
|
|
@ -74,7 +64,7 @@ class PodcastModel extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Podcast>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Podcast::class;
|
protected $returnType = Podcast::class;
|
||||||
|
|
||||||
|
|
@ -173,7 +163,7 @@ class PodcastModel extends Model
|
||||||
/**
|
/**
|
||||||
* @return Podcast[]
|
* @return Podcast[]
|
||||||
*/
|
*/
|
||||||
public function getAllPodcasts(string $orderBy = null): array
|
public function getAllPodcasts(?string $orderBy = null): array
|
||||||
{
|
{
|
||||||
$prefix = $this->db->getPrefix();
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
||||||
|
|
@ -185,8 +175,8 @@ class PodcastModel extends Model
|
||||||
->where(
|
->where(
|
||||||
'`' . $prefix . 'fediverse_posts`.`published_at` <= UTC_TIMESTAMP()',
|
'`' . $prefix . 'fediverse_posts`.`published_at` <= UTC_TIMESTAMP()',
|
||||||
null,
|
null,
|
||||||
false
|
false,
|
||||||
)->orWhere('fediverse_posts.published_at', null)
|
)->orWhere('fediverse_posts.published_at')
|
||||||
->groupEnd()
|
->groupEnd()
|
||||||
->groupBy('podcasts.actor_id')
|
->groupBy('podcasts.actor_id')
|
||||||
->orderBy('max_published_at', 'DESC');
|
->orderBy('max_published_at', 'DESC');
|
||||||
|
|
@ -319,7 +309,8 @@ class PodcastModel extends Model
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId);
|
$secondsToNextUnpublishedEpisode = new EpisodeModel()
|
||||||
|
->getSecondsToNextUnpublishedEpisode($podcastId);
|
||||||
|
|
||||||
cache()
|
cache()
|
||||||
->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
|
->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
|
||||||
|
|
@ -335,7 +326,8 @@ class PodcastModel extends Model
|
||||||
*/
|
*/
|
||||||
public function clearCache(array $data): array
|
public function clearCache(array $data): array
|
||||||
{
|
{
|
||||||
$podcast = (new self())->getPodcastById((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
|
// delete cache for users' podcasts
|
||||||
cache()
|
cache()
|
||||||
|
|
@ -399,21 +391,22 @@ class PodcastModel extends Model
|
||||||
$domain =
|
$domain =
|
||||||
$url->getHost() . ($url->getPort() ? ':' . $url->getPort() : '');
|
$url->getHost() . ($url->getPort() ? ':' . $url->getPort() : '');
|
||||||
|
|
||||||
$actorId = (new ActorModel())->insert(
|
$actorId = new ActorModel()
|
||||||
[
|
->insert(
|
||||||
'uri' => url_to('podcast-activity', $username),
|
[
|
||||||
'username' => $username,
|
'uri' => url_to('podcast-activity', $username),
|
||||||
'domain' => $domain,
|
'username' => $username,
|
||||||
'private_key' => $privatekey,
|
'domain' => $domain,
|
||||||
'public_key' => $publickey,
|
'private_key' => $privatekey,
|
||||||
'display_name' => $data['data']['title'],
|
'public_key' => $publickey,
|
||||||
'summary' => $data['data']['description_html'],
|
'display_name' => $data['data']['title'],
|
||||||
'inbox_url' => url_to('inbox', $username),
|
'summary' => $data['data']['description_html'],
|
||||||
'outbox_url' => url_to('outbox', $username),
|
'inbox_url' => url_to('inbox', $username),
|
||||||
'followers_url' => url_to('followers', $username),
|
'outbox_url' => url_to('outbox', $username),
|
||||||
],
|
'followers_url' => url_to('followers', $username),
|
||||||
true,
|
],
|
||||||
);
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
$data['data']['actor_id'] = $actorId;
|
$data['data']['actor_id'] = $actorId;
|
||||||
|
|
||||||
|
|
@ -427,10 +420,12 @@ class PodcastModel extends Model
|
||||||
*/
|
*/
|
||||||
protected function setActorAvatar(array $data): array
|
protected function setActorAvatar(array $data): array
|
||||||
{
|
{
|
||||||
$podcast = (new self())->getPodcastById((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) {
|
if ($podcast instanceof Podcast) {
|
||||||
$podcastActor = (new ActorModel())->find($podcast->actor_id);
|
$podcastActor = new ActorModel()
|
||||||
|
->find($podcast->actor_id);
|
||||||
|
|
||||||
if (! $podcastActor instanceof Actor) {
|
if (! $podcastActor instanceof Actor) {
|
||||||
return $data;
|
return $data;
|
||||||
|
|
@ -439,7 +434,8 @@ class PodcastModel extends Model
|
||||||
$podcastActor->avatar_image_url = $podcast->cover->federation_url;
|
$podcastActor->avatar_image_url = $podcast->cover->federation_url;
|
||||||
$podcastActor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
|
$podcastActor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
|
||||||
|
|
||||||
(new ActorModel())->update($podcast->actor_id, $podcastActor);
|
new ActorModel()
|
||||||
|
->update($podcast->actor_id, $podcastActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
|
|
@ -452,7 +448,8 @@ class PodcastModel extends Model
|
||||||
*/
|
*/
|
||||||
protected function updatePodcastActor(array $data): array
|
protected function updatePodcastActor(array $data): array
|
||||||
{
|
{
|
||||||
$podcast = (new self())->getPodcastById((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) {
|
if ($podcast instanceof Podcast) {
|
||||||
$actorModel = new ActorModel();
|
$actorModel = new ActorModel();
|
||||||
|
|
@ -488,7 +485,7 @@ class PodcastModel extends Model
|
||||||
{
|
{
|
||||||
if (! array_key_exists(
|
if (! array_key_exists(
|
||||||
'guid',
|
'guid',
|
||||||
$data['data']
|
$data['data'],
|
||||||
) || $data['data']['guid'] === null || $data['data']['guid'] === '') {
|
) || $data['data']['guid'] === null || $data['data']['guid'] === '') {
|
||||||
$uuid = service('uuid');
|
$uuid = service('uuid');
|
||||||
$feedUrl = url_to('podcast-rss-feed', $data['data']['handle']);
|
$feedUrl = url_to('podcast-rss-feed', $data['data']['handle']);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use Modules\Fediverse\Models\PostModel as FediversePostModel;
|
||||||
class PostModel extends FediversePostModel
|
class PostModel extends FediversePostModel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var class-string<Post>
|
||||||
*/
|
*/
|
||||||
protected $returnType = Post::class;
|
protected $returnType = Post::class;
|
||||||
|
|
||||||
|
|
@ -32,6 +32,7 @@ class PostModel extends FediversePostModel
|
||||||
'episode_id',
|
'episode_id',
|
||||||
'message',
|
'message',
|
||||||
'message_html',
|
'message_html',
|
||||||
|
'is_private',
|
||||||
'favourites_count',
|
'favourites_count',
|
||||||
'reblogs_count',
|
'reblogs_count',
|
||||||
'replies_count',
|
'replies_count',
|
||||||
|
|
@ -49,7 +50,7 @@ class PostModel extends FediversePostModel
|
||||||
return $this->where([
|
return $this->where([
|
||||||
'episode_id' => $episodeId,
|
'episode_id' => $episodeId,
|
||||||
])
|
])
|
||||||
->where('in_reply_to_id', null)
|
->where('in_reply_to_id')
|
||||||
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
|
||||||
->orderBy('published_at', 'DESC')
|
->orderBy('published_at', 'DESC')
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @icon('funding:buymeacoffee')
|
|
||||||
* @icon('funding:donorbox')
|
|
||||||
* @icon('funding:gofundme')
|
|
||||||
* @icon('funding:helloasso')
|
|
||||||
* @icon('funding:indiegogo')
|
|
||||||
* @icon('funding:kickstarter')
|
|
||||||
* @icon('funding:kisskissbankbank')
|
|
||||||
* @icon('funding:kofi')
|
|
||||||
* @icon('funding:liberapay')
|
|
||||||
* @icon('funding:patreon')
|
|
||||||
* @icon('funding:paypal')
|
|
||||||
* @icon('funding:tipeee')
|
|
||||||
* @icon('funding:ulule')
|
|
||||||
*/
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @icon('podcasting:amazon')
|
|
||||||
* @icon('podcasting:antennapod')
|
|
||||||
* @icon('podcasting:anytime')
|
|
||||||
* @icon('podcasting:apple')
|
|
||||||
* @icon('podcasting:blubrry')
|
|
||||||
* @icon('podcasting:breez')
|
|
||||||
* @icon('podcasting:castamatic')
|
|
||||||
* @icon('podcasting:castbox')
|
|
||||||
* @icon('podcasting:castopod')
|
|
||||||
* @icon('podcasting:castro')
|
|
||||||
* @icon('podcasting:deezer')
|
|
||||||
* @icon('podcasting:episodes-fm')
|
|
||||||
* @icon('podcasting:fountain')
|
|
||||||
* @icon('podcasting:fyyd')
|
|
||||||
* @icon('podcasting:gpodder')
|
|
||||||
* @icon('podcasting:ivoox')
|
|
||||||
* @icon('podcasting:listennotes')
|
|
||||||
* @icon('podcasting:overcast')
|
|
||||||
* @icon('podcasting:playerfm')
|
|
||||||
* @icon('podcasting:plink')
|
|
||||||
* @icon('podcasting:pocketcasts')
|
|
||||||
* @icon('podcasting:podbean')
|
|
||||||
* @icon('podcasting:podcastaddict')
|
|
||||||
* @icon('podcasting:podcastguru')
|
|
||||||
* @icon('podcasting:podcastindex')
|
|
||||||
* @icon('podcasting:podchaser')
|
|
||||||
* @icon('podcasting:podcloud')
|
|
||||||
* @icon('podcasting:podfriend')
|
|
||||||
* @icon('podcasting:podinstall')
|
|
||||||
* @icon('podcasting:podlink')
|
|
||||||
* @icon('podcasting:podlp')
|
|
||||||
* @icon('podcasting:podnews')
|
|
||||||
* @icon('podcasting:podtail')
|
|
||||||
* @icon('podcasting:podverse')
|
|
||||||
* @icon('podcasting:radiopublic')
|
|
||||||
* @icon('podcasting:sphinxchat')
|
|
||||||
* @icon('podcasting:spotify')
|
|
||||||
* @icon('podcasting:spreaker')
|
|
||||||
* @icon('podcasting:truefans')
|
|
||||||
* @icon('podcasting:tsacdop')
|
|
||||||
* @icon('podcasting:tunein')
|
|
||||||
* @icon('podcasting:youtube-music')
|
|
||||||
*/
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @icon('social:bluesky')
|
|
||||||
* @icon('social:discord')
|
|
||||||
* @icon('social:facebook')
|
|
||||||
* @icon('social:funkwhale')
|
|
||||||
* @icon('social:instagram')
|
|
||||||
* @icon('social:linkedin')
|
|
||||||
* @icon('social:mastodon')
|
|
||||||
* @icon('social:matrix')
|
|
||||||
* @icon('social:misskey')
|
|
||||||
* @icon('social:mobilizon')
|
|
||||||
* @icon('social:peertube')
|
|
||||||
* @icon('social:pixelfed')
|
|
||||||
* @icon('social:pleroma')
|
|
||||||
* @icon('social:plume')
|
|
||||||
* @icon('social:slack')
|
|
||||||
* @icon('social:telegram')
|
|
||||||
* @icon('social:threads')
|
|
||||||
* @icon('social:tiktok')
|
|
||||||
* @icon('social:twitch')
|
|
||||||
* @icon('social:writefreely')
|
|
||||||
* @icon('social:x')
|
|
||||||
* @icon('social:youtube')
|
|
||||||
*/
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59 40">
|
|
||||||
<path d="M52.801 38.23h-8.853s-.93-1.975-1.306-2.671c-.378-.697-1.278-.668-1.278-.668H17.33s-.871-.116-1.307.668c-.464.784-1.335 2.67-1.335 2.67H5.98c-2.003 0-3.658-1.625-3.658-3.628V5.893c0-2.003 1.626-3.658 3.629-3.658h46.821c2.003 0 3.658 1.626 3.658 3.629v28.708c.029 2.003-1.626 3.657-3.629 3.657Z" fill="#009486"/>
|
|
||||||
<path d="M17.01 10.014h24.703c4.267-.028 7.721 3.426 7.692 7.722 0 4.238-3.454 7.692-7.692 7.692H17.01c-4.238 0-7.692-3.454-7.692-7.692 0-4.238 3.454-7.722 7.692-7.722Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M39.768 14.804a3.773 3.773 0 0 0-3.774 3.777c0 .861.298 1.656.795 2.319 0 0 1.29-.96 3.111-.96 1.357 0 2.946.827 2.946.827.43-.63.695-1.358.695-2.186a3.773 3.773 0 0 0-3.773-3.777Zm-20.9 0a3.773 3.773 0 0 0-3.774 3.777c0 .828.265 1.557.695 2.186 0 0 1.59-.828 2.946-.828 1.821 0 3.112.96 3.112.96a3.707 3.707 0 0 0 .794-2.318 3.773 3.773 0 0 0-3.773-3.777Z" fill="#009486"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 966 B |
|
|
@ -1,6 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59 40">
|
|
||||||
<path d="M53.024 38.23H43.55s-.35-.726-.728-1.423c-.38-.697-1.283-.668-1.283-.668H17.403s-.875-.116-1.312.668a16.505 16.505 0 0 0-.758 1.422H6.005c-2.011 0-3.673-1.625-3.673-3.628V5.893c0-2.003 1.632-3.658 3.644-3.658h47.02c2.01 0 3.672 1.626 3.672 3.629v28.708c.03 2.003-1.632 3.657-3.644 3.657Z" fill="#009486"/>
|
|
||||||
<path d="M41.889 10.014H17.082c-4.256 0-7.725 3.484-7.725 7.722s3.47 7.692 7.725 7.692h24.807c4.256 0 7.725-3.454 7.725-7.692a7.655 7.655 0 0 0-7.725-7.722Zm-21.98 9.928s-1.136-.842-2.74-.842c-1.195 0-2.594.726-2.594.726a3.333 3.333 0 0 1-.612-1.916 3.315 3.315 0 0 1 3.323-3.31 3.315 3.315 0 0 1 3.323 3.31c0 .784-.262 1.48-.7 2.032Zm9.591 1.916c-3.644 0-3.498-2.787-3.498-2.787-.058-.522.612-.697.845-.377.117.174.117.174.204.493.496 1.713 2.449 1.626 2.449 1.626s1.953.116 2.449-1.626c.087-.29.087-.32.204-.493.233-.29.903-.145.845.377 0 0 .146 2.787-3.498 2.787Zm14.896-2.003s-1.4-.726-2.595-.726c-1.603 0-2.74.842-2.74.842a3.238 3.238 0 0 1-.7-2.032 3.315 3.315 0 0 1 3.324-3.31 3.315 3.315 0 0 1 3.323 3.31 3.014 3.014 0 0 1-.612 1.916Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M41.714 14.63a3.315 3.315 0 0 0-3.323 3.309c0 .755.262 1.451.7 2.032 0 0 1.136-.842 2.74-.842 1.195 0 2.594.726 2.594.726.379-.552.612-1.19.612-1.916a3.315 3.315 0 0 0-3.323-3.31ZM17.286 14.63a3.315 3.315 0 0 0-3.323 3.309c0 .726.233 1.364.612 1.916 0 0 1.4-.726 2.594-.726 1.604 0 2.74.842 2.74.842.438-.552.7-1.248.7-2.032a3.315 3.315 0 0 0-3.323-3.31Z" fill="#009486"/>
|
|
||||||
<path d="M13.73 6.763c-1.837-.493-3.41.61-4.285 2.12-.204.348-.059.638.145.754.292.087.496.03.817-.435.641-1.132 1.72-1.77 2.944-1.539 0 0 .845.262.962-.29.087-.348-.233-.522-.583-.61ZM46.611 7.925c-.03.551.845.551.845.551 1.225.116 1.983.668 2.274 1.945.175.551.35.639.67.639.234-.03.467-.29.35-.697-.408-1.684-1.486-2.845-3.41-2.874-.35-.029-.7.058-.729.436Z" fill="#E7F9E4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,40 +0,0 @@
|
||||||
<svg width="232" height="260" viewBox="0 0 232 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M79.5436 200.875L141.5 200.376C143.831 200.376 145.664 198.377 145.664 196.045C145.664 193.714 143.665 191.882 141.333 191.882L79.3771 192.381C77.0454 192.381 75.2134 194.38 75.2134 196.712C75.3799 198.877 77.212 200.875 79.5436 200.875Z" fill="#006D60"/>
|
|
||||||
<path d="M135.171 191.882L86.0393 192.215C86.0393 192.215 98.5305 194.38 111.355 194.213C124.512 194.213 135.171 191.882 135.171 191.882Z" fill="#00564A"/>
|
|
||||||
<path d="M166.443 169.873L54.356 170.738L54.1786 147.754L166.266 146.889L166.443 169.873Z" fill="#00564A"/>
|
|
||||||
<path d="M50.5645 170.064C50.731 180.889 59.5581 189.716 70.3838 189.716L150.66 189.05C161.486 189.05 170.146 179.89 170.146 169.064V165.9C170.146 163.235 167.981 161.07 165.317 161.07L55.3944 161.903C52.7296 161.903 50.5645 164.068 50.5645 166.733V170.064Z" fill="#009486"/>
|
|
||||||
<path d="M164.987 156.203L55.3978 157.049L55.3952 156.716L164.984 155.87L164.987 156.203Z" fill="#2B6B5F"/>
|
|
||||||
<path d="M165.008 157.622L55.419 158.468L55.4164 158.135L165.005 157.289L165.008 157.622Z" fill="#2B6B5F"/>
|
|
||||||
<path d="M165.03 158.873L55.4407 159.719L55.4382 159.386L165.027 158.54L165.03 158.873Z" fill="#2B6B5F"/>
|
|
||||||
<path d="M111.688 189.383L150.494 189.05C161.319 188.884 170.146 179.89 169.98 169.064C169.98 169.064 168.314 177.891 156.323 182.388C147.496 185.719 135.004 185.053 111.521 185.22C88.0377 185.386 73.2149 186.219 64.2213 183.054C52.2297 178.724 50.3977 170.064 50.3977 170.064C50.5643 180.889 59.3913 189.716 70.217 189.716L111.688 189.383Z" fill="#006D60"/>
|
|
||||||
<path d="M72.0488 114.103L72.2154 111.938L77.0453 103.611C77.0453 103.611 77.5449 102.445 80.3763 102.611C81.7087 102.611 144.498 106.109 145.83 106.109C148.828 106.276 149.161 107.441 149.161 107.441L151.826 113.77L151.659 115.935L72.0488 114.103Z" fill="#00564A"/>
|
|
||||||
<path d="M79.0291 104.9L79.0472 104.567L146.731 108.252L146.713 108.584L79.0291 104.9Z" fill="#2B6B5F"/>
|
|
||||||
<path d="M77.7524 106.133L77.7705 105.8L147.949 109.621L147.931 109.953L77.7524 106.133Z" fill="#2B6B5F"/>
|
|
||||||
<path d="M45.2346 103.944L72.5486 105.443L74.8803 101.446C76.0462 99.447 78.711 99.7801 78.711 99.7801L148.162 103.611C148.162 103.611 150.66 103.444 151.826 105.776C152.992 108.108 153.824 109.94 153.824 109.94L180.639 111.439C186.468 111.772 191.464 107.275 191.798 101.446L196.294 18.5043C196.628 12.6751 192.131 7.67862 186.301 7.34552L51.2304 0.0173753C45.4012 -0.315722 40.4047 4.1811 40.0716 10.0103L35.5748 92.9516C34.9086 98.6143 39.4054 103.611 45.2346 103.944Z" fill="#009486"/>
|
|
||||||
<path d="M149.994 72.6327L77.7117 68.6355C77.7117 68.6355 95.8655 72.7992 114.685 73.7985C134.172 74.9644 149.994 72.6327 149.994 72.6327Z" fill="#006D60"/>
|
|
||||||
<path d="M58.7252 45.3186C58.059 57.6432 67.5523 68.1358 79.7103 68.802L150.993 72.6326C163.318 73.2988 173.81 63.9721 174.477 51.6475C175.143 39.3229 165.649 28.8303 153.491 28.1641L82.2085 24.3335C69.8839 23.5007 59.3914 32.994 58.7252 45.3186Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M171.334 27.0569C169.538 22.3913 165.594 18.6476 160.122 19.1365C159.124 19.3068 158.18 19.6345 158.138 20.5304C158.158 22.1103 160.681 21.7632 160.681 21.7632C164.211 21.7723 167.082 23.949 168.338 27.5679C169.041 29.0867 169.622 29.2375 170.464 29.1218C171.305 29.0061 171.716 28.1582 171.334 27.0569Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M71.2163 105.443L45.2347 104.11C39.4055 103.777 34.9087 98.6143 35.4083 92.785L38.5727 36.325L41.4041 92.785C41.4041 92.785 40.4048 98.9473 49.7315 100.946L71.2163 105.443Z" fill="#006D60"/>
|
|
||||||
<path d="M125.486 54.405C125.486 54.405 120.823 53.9054 115.66 54.405C110.497 54.9046 107.832 56.0705 107.832 56.0705C106.833 56.0705 106 55.4043 106 54.5715C106 53.5723 106.5 53.0726 107.499 52.7395C107.499 52.7395 112.495 51.4071 115.493 51.074C118.491 50.9075 125.32 51.074 125.32 51.074C126.319 51.074 127.152 51.7402 127.152 52.573C126.985 53.4057 126.319 54.2384 125.486 54.405Z" fill="#006D60"/>
|
|
||||||
<ellipse cx="148" cy="48" rx="7" ry="9" fill="#006D60"/>
|
|
||||||
<ellipse cx="86" cy="44" rx="7" ry="9" fill="#006D60"/>
|
|
||||||
<path d="M74.5763 17.4708C74.5763 17.4708 77.0259 17.961 77.1895 16.2952C77.2222 15.2827 76.1117 14.8906 75.2625 14.8905C69.8079 14.1386 66.182 16.9798 64.3849 21.7157C63.9928 22.8262 64.5154 23.6102 65.1686 23.7409C66.1485 23.937 66.5404 23.6757 67.3572 22.1407C68.7621 18.5153 71.0487 17.2744 74.5763 17.4708Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M178.807 139.086C180.472 138.919 181.805 136.421 181.638 133.423L181.305 123.097C181.138 119.266 178.807 117.601 169.48 118.1C166.815 118.267 159.487 118.767 159.487 118.767C157.822 118.933 156.489 121.431 156.656 124.429L157.655 140.418C158.321 140.418 178.807 139.086 178.807 139.086Z" fill="#006D60"/>
|
|
||||||
<path d="M213.194 166.667L214.283 166.218C219.791 163.82 222.03 156.274 219.633 150.766L212.03 132.627C209.337 126.094 199.032 113.145 189.668 117.005L187.491 117.902C181.983 120.3 183.714 124.809 186.111 130.317L198.894 160.714C201.228 166.376 207.75 168.911 213.194 166.667Z" fill="#009486"/>
|
|
||||||
<path d="M213.194 166.667C213.194 166.667 207.161 169.026 201.764 159.022C199.533 154.846 188.429 128.215 187.455 124.923C186.159 120.234 187.491 117.902 187.491 117.902C181.983 120.3 183.714 124.809 186.111 130.317L198.894 160.714C201.228 166.376 207.75 168.911 213.194 166.667Z" fill="#006D60"/>
|
|
||||||
<path d="M215.827 144.552L203.234 149.838L203.363 150.145L215.956 144.859L215.827 144.552Z" fill="#71AFA3"/>
|
|
||||||
<path d="M216.894 147.07L204.301 152.356L204.43 152.663L217.023 147.377L216.894 147.07Z" fill="#71AFA3"/>
|
|
||||||
<path d="M217.89 149.432L205.298 154.718L205.427 155.025L218.019 149.739L217.89 149.432Z" fill="#71AFA3"/>
|
|
||||||
<path d="M42.403 136.421C40.7375 136.254 39.5716 133.589 40.0713 130.592L41.0706 120.432C41.5702 116.601 43.9019 115.103 53.2286 116.268C55.8934 116.601 63.055 117.601 63.055 117.601C64.7205 117.767 65.8864 120.432 65.3867 123.43L63.2216 139.252C62.8885 139.252 42.403 136.421 42.403 136.421Z" fill="#006D60"/>
|
|
||||||
<path d="M12.5417 160.989L11.4921 160.606C5.74762 158.749 3.05655 151.346 4.9138 145.601L11.1425 126.784C13.4015 120.155 22.5527 106.368 32.1268 109.463L34.3915 110.21C40.1359 112.067 38.6615 116.762 36.8042 122.507L26.4782 153.862C24.4744 159.791 18.2862 162.846 12.5417 160.989Z" fill="#009486"/>
|
|
||||||
<path d="M12.5417 160.989C12.5417 160.989 18.6361 162.974 23.2915 152.383C25.1348 147.981 34.2416 120.621 34.8557 117.198C35.8148 112.394 34.3915 110.21 34.3915 110.21C40.136 112.067 38.6615 116.762 36.8043 122.507L26.4782 153.863C24.4744 159.791 18.2862 162.846 12.5417 160.989Z" fill="#006D60"/>
|
|
||||||
<path d="M8.11157 139.649L21.0859 143.913L21.1899 143.596L8.21557 139.332L8.11157 139.649Z" fill="#71AFA3"/>
|
|
||||||
<path d="M7.22778 142.062L20.2021 146.326L20.3061 146.009L7.33178 141.745L7.22778 142.062Z" fill="#71AFA3"/>
|
|
||||||
<path d="M6.50293 144.527L19.4772 148.791L19.5812 148.475L6.60693 144.211L6.50293 144.527Z" fill="#71AFA3"/>
|
|
||||||
<path d="M58.7251 152.076C47.5663 151.91 45.4012 147.746 45.4012 136.92C45.4012 136.92 45.0681 118.767 45.0681 116.601C45.0681 112.937 52.5628 110.106 59.891 110.273L162.818 112.438C170.146 112.604 177.474 115.602 177.308 119.433C177.308 121.598 176.142 139.585 176.142 139.585C175.642 150.411 173.311 154.408 162.152 154.241L58.7251 152.076Z" fill="#009486"/>
|
|
||||||
<path d="M110.355 260C159.198 260 198.793 255.004 198.793 248.841C198.793 242.678 159.198 237.682 110.355 237.682C61.5125 237.682 21.9177 242.678 21.9177 248.841C21.9177 255.004 61.5125 260 110.355 260Z" fill="#000000" opacity=".05"/>
|
|
||||||
<path d="M93.7003 129.426V128.26H94.8662C94.8662 128.427 94.8662 128.427 94.6996 128.926V129.259H96.3651L96.8648 129.759C96.8648 129.926 96.8648 129.926 96.6982 130.259C96.6982 131.591 96.5317 132.59 96.3651 132.923C96.1986 133.423 95.8655 133.59 95.1993 133.59H94.3665C94.3665 133.09 94.3665 132.923 94.2 132.59C94.5331 132.757 94.8662 132.757 95.0327 132.757C95.3658 132.757 95.3658 132.59 95.5324 132.091C95.5324 131.758 95.6989 130.925 95.6989 130.425H94.5331C94.5331 130.758 94.5331 130.758 94.3665 131.091C94.0334 132.257 93.5338 133.09 92.2014 133.923C91.8683 133.423 91.7017 133.256 91.3687 132.923C92.2014 132.59 92.701 132.091 93.0341 131.424C93.2007 131.091 93.2007 130.925 93.3672 130.425H91.7017V129.426H93.7003ZM102.028 133.923C101.528 133.256 101.028 132.757 100.362 132.091C99.6961 132.757 99.0299 133.256 98.0306 133.756C97.8641 133.256 97.6975 133.09 97.3644 132.757C98.1971 132.424 98.8633 132.091 99.363 131.591C99.8626 131.091 100.362 130.592 100.695 129.925H98.1971V128.926H101.695L102.194 129.426C102.028 129.592 102.028 129.592 102.028 129.759C101.695 130.425 101.528 130.925 101.028 131.424C101.695 131.924 102.028 132.257 102.861 133.09L102.028 133.923ZM104.026 133.923V132.923L104.193 129.426V128.593H105.359V130.259C106.358 130.592 107.191 130.925 108.024 131.591L107.357 132.59C106.858 132.091 106.025 131.758 105.525 131.424C105.359 131.258 105.359 131.258 105.192 131.258V133.756L104.026 133.923ZM108.69 130.925C109.023 130.925 109.356 130.925 109.856 131.091H113.853V132.257H108.69V130.925ZM116.351 131.424V131.591C116.184 132.59 115.851 133.256 115.352 133.923L114.352 133.423C114.852 132.757 115.185 131.924 115.352 131.091L116.351 131.424ZM120.015 130.925H117.85V134.422H116.684V130.758H114.685V129.759H116.851V128.926H118.016V129.759H119.349C119.182 129.592 119.182 129.426 119.182 129.259C119.182 128.926 119.515 128.593 119.848 128.593C120.182 128.593 120.515 128.926 120.515 129.259C120.515 129.426 120.348 129.759 120.182 129.925L120.015 130.925ZM119.182 131.091C119.349 131.924 119.682 132.923 120.182 133.423L119.182 133.923C118.683 133.09 118.516 132.59 118.35 131.591C118.35 131.424 118.35 131.424 118.183 131.258L119.182 131.091ZM119.349 129.426C119.349 129.592 119.515 129.759 119.682 129.759C119.848 129.759 120.015 129.592 120.015 129.426C120.015 129.259 119.848 129.093 119.682 129.093C119.515 129.093 119.349 129.259 119.349 129.426ZM121.681 130.425L122.18 131.924L121.181 132.257C121.014 131.591 120.848 131.258 120.681 130.758L121.681 130.425ZM125.178 130.592C125.178 130.758 125.178 130.758 125.011 130.925C124.845 131.758 124.512 132.424 124.179 132.923C123.846 133.423 123.346 133.756 122.846 134.089C122.68 134.256 122.513 134.256 122.18 134.422C122.014 134.089 121.847 133.923 121.514 133.59C122.68 133.09 123.346 132.59 123.846 131.591C124.012 131.091 124.179 130.758 124.179 130.259L125.178 130.592ZM123.013 130.259C123.346 130.925 123.346 131.091 123.513 131.758L122.513 132.091L122.014 130.592L123.013 130.259ZM126.51 134.589V133.59L126.677 130.092V129.259H127.843V130.925C128.842 131.258 129.675 131.758 130.508 132.257L129.841 133.256C129.342 132.923 128.676 132.424 128.009 132.091C127.843 132.091 127.843 132.091 127.843 131.924V134.422L126.51 134.589ZM129.342 129.592C129.508 129.926 129.675 130.092 129.841 130.425L129.342 130.758C129.175 130.425 129.009 130.092 128.842 129.925L129.342 129.592ZM130.175 129.259C130.341 129.426 130.508 129.759 130.841 130.092L130.341 130.425C130.175 130.092 130.008 129.759 129.841 129.592L130.175 129.259Z" fill="#00564A"/>
|
|
||||||
<path d="M104.193 139.419C104.026 139.419 104.026 139.419 103.86 139.585H103.526C103.193 139.585 102.86 139.419 102.694 139.252C102.527 139.086 102.361 138.753 102.361 138.253C102.361 137.753 102.527 137.42 102.694 137.254C102.86 137.087 103.193 136.921 103.526 136.921H103.86C104.026 136.921 104.026 136.921 104.193 137.087V137.587C104.026 137.42 104.026 137.42 103.86 137.42H103.526C103.36 137.42 103.193 137.42 103.027 137.587C103.193 137.587 103.193 137.92 103.193 138.086C103.193 138.42 103.193 138.586 103.36 138.753C103.526 138.919 103.526 138.919 103.86 138.919H104.193C104.359 138.919 104.359 138.753 104.526 138.753L104.193 139.419ZM105.692 136.921H106.191V138.586H106.524V139.086H106.191V139.585H105.692V139.086H104.692V138.586L105.692 136.921ZM105.692 137.42L105.025 138.42H105.692V137.42ZM107.024 136.921H108.523V137.42H107.524V137.92H107.857C108.19 137.92 108.356 138.086 108.523 138.253C108.689 138.42 108.689 138.586 108.689 138.919C108.689 139.252 108.523 139.419 108.356 139.585C108.19 139.752 107.857 139.752 107.69 139.752H107.357C107.191 139.752 107.191 139.752 107.024 139.585V139.086C107.191 139.086 107.191 139.252 107.357 139.252H107.69C107.857 139.252 108.023 139.252 108.19 139.086C108.356 139.086 108.356 138.919 108.356 138.753C108.356 138.586 108.356 138.42 108.19 138.42C108.023 138.253 108.023 138.253 107.857 138.253H107.524C107.357 138.253 107.357 138.253 107.191 138.42L107.024 136.921ZM109.189 136.921H110.855V137.254L109.855 139.585H109.356L110.188 137.42H109.023L109.189 136.921ZM111.188 138.42C111.188 137.92 111.354 137.587 111.354 137.42C111.521 137.254 111.687 137.087 112.02 137.087C112.354 137.087 112.52 137.254 112.687 137.42C112.853 137.587 112.853 137.92 112.853 138.42C112.853 138.919 112.687 139.252 112.687 139.419C112.52 139.585 112.354 139.752 112.02 139.752C111.687 139.752 111.521 139.585 111.354 139.419C111.188 139.252 111.188 138.753 111.188 138.42ZM112.187 137.42C112.02 137.42 112.02 137.42 111.854 137.587C111.854 137.753 111.687 137.92 111.687 138.253V138.586L112.354 137.587C112.354 137.587 112.354 137.42 112.187 137.42ZM111.854 139.086C111.854 139.252 112.02 139.252 112.187 139.252C112.354 139.252 112.354 139.252 112.52 139.086C112.52 138.919 112.687 138.753 112.687 138.42V138.086L111.854 139.086ZM113.519 137.087H114.186C114.519 137.087 114.852 137.087 115.018 137.254C115.185 137.42 115.185 137.587 115.185 137.92C115.185 138.253 115.018 138.42 114.852 138.586C114.685 138.753 114.352 138.753 114.019 138.753H113.852V139.752H113.353L113.519 137.087ZM114.019 137.587V138.42H114.519C114.519 138.42 114.685 138.253 114.685 138.086C114.685 137.92 114.685 137.92 114.519 137.753C114.519 137.753 114.352 137.587 114.186 137.587H114.019ZM115.684 138.42C115.684 137.92 115.851 137.587 115.851 137.42C116.018 137.254 116.184 137.087 116.517 137.087C116.85 137.087 117.017 137.254 117.183 137.42C117.35 137.587 117.35 137.92 117.35 138.42C117.35 138.919 117.183 139.252 117.183 139.419C117.017 139.585 116.85 139.752 116.517 139.752C116.184 139.752 116.018 139.585 115.851 139.419C115.684 139.252 115.684 138.919 115.684 138.42ZM116.517 137.587C116.351 137.587 116.351 137.587 116.184 137.753C116.184 137.92 116.018 138.086 116.018 138.42V138.753L116.684 137.753C116.684 137.587 116.684 137.587 116.517 137.587ZM116.184 139.252C116.184 139.419 116.351 139.419 116.517 139.419C116.684 139.419 116.684 139.419 116.85 139.252C116.85 139.086 117.017 138.919 117.017 138.586V138.253L116.184 139.252C116.184 139.086 116.184 139.086 116.184 139.252ZM117.85 137.254H118.349C118.849 137.254 119.015 137.42 119.349 137.587C119.515 137.753 119.682 138.086 119.682 138.586C119.682 139.086 119.515 139.419 119.349 139.585C119.182 139.752 118.849 139.918 118.349 139.918H117.85V137.254ZM118.349 137.587V139.252H118.516C118.682 139.252 118.849 139.252 119.015 139.086C119.182 138.919 119.182 138.753 119.182 138.42C119.182 138.086 119.182 137.92 119.015 137.753C118.849 137.753 118.682 137.753 118.349 137.587Z" fill="#00564A"/>
|
|
||||||
<path d="M93.7003 129.093V127.927H94.8662C94.8662 128.093 94.8662 128.093 94.6996 128.593V128.926H96.3651L96.8648 129.426C96.8648 129.592 96.8648 129.592 96.6982 129.925C96.6982 131.258 96.5317 132.257 96.3651 132.59C96.1986 133.09 95.8655 133.256 95.1993 133.256H94.3665C94.3665 132.757 94.3665 132.59 94.2 132.257C94.5331 132.424 94.8662 132.424 95.0327 132.424C95.3658 132.424 95.3658 132.257 95.5324 131.757C95.5324 131.424 95.6989 130.592 95.6989 130.092H94.5331C94.5331 130.425 94.5331 130.425 94.3665 130.758C94.0334 131.924 93.5338 132.757 92.2014 133.59C91.8683 133.09 91.7017 132.923 91.3687 132.59C92.2014 132.257 92.701 131.758 93.0341 131.091C93.2007 130.758 93.2007 130.592 93.3672 130.092H91.7017V129.093H93.7003ZM102.028 133.59C101.528 132.923 101.028 132.424 100.362 131.757C99.6961 132.424 99.0299 132.923 98.0306 133.423C97.8641 132.923 97.6975 132.757 97.3644 132.424C98.1971 132.091 98.8633 131.758 99.363 131.258C99.8626 130.758 100.362 130.259 100.695 129.592H98.1971V128.593H101.695L102.194 129.093C102.028 129.259 102.028 129.259 102.028 129.426C101.695 130.092 101.528 130.592 101.028 131.091C101.695 131.591 102.028 131.924 102.861 132.757L102.028 133.59ZM104.026 133.59V132.59L104.193 129.093V128.26H105.359V129.925C106.358 130.259 107.191 130.592 108.024 131.258L107.357 132.257C106.858 131.757 106.025 131.424 105.525 131.091C105.359 130.925 105.359 130.925 105.192 130.925V133.423L104.026 133.59ZM108.69 130.425C109.023 130.425 109.356 130.425 109.856 130.592H113.853V131.757H108.69V130.425ZM116.351 130.925V131.091C116.184 132.091 115.851 132.757 115.352 133.423L114.352 132.923C114.852 132.257 115.185 131.424 115.352 130.592L116.351 130.925ZM120.015 130.592H117.85V134.089H116.684V130.425H114.685V129.426H116.851V128.593H118.016V129.426H119.349C119.182 129.259 119.182 129.093 119.182 128.926C119.182 128.593 119.515 128.26 119.848 128.26C120.182 128.26 120.515 128.593 120.515 128.926C120.515 129.093 120.348 129.426 120.182 129.592L120.015 130.592ZM119.182 130.758C119.349 131.591 119.682 132.59 120.182 133.09L119.182 133.59C118.683 132.757 118.516 132.257 118.35 131.258C118.35 131.091 118.35 131.091 118.183 130.925L119.182 130.758ZM119.349 129.093C119.349 129.259 119.515 129.426 119.682 129.426C119.848 129.426 120.015 129.259 120.015 129.093C120.015 128.926 119.848 128.76 119.682 128.76C119.515 128.76 119.349 128.926 119.349 129.093ZM121.681 130.092L122.18 131.591L121.181 131.924C121.014 131.258 120.848 130.925 120.681 130.425L121.681 130.092ZM125.178 130.259C125.178 130.425 125.178 130.425 125.011 130.592C124.845 131.424 124.512 132.091 124.179 132.59C123.846 133.09 123.346 133.423 122.846 133.756C122.68 133.923 122.513 133.923 122.18 134.089C122.014 133.756 121.847 133.59 121.514 133.256C122.68 132.757 123.346 132.257 123.846 131.258C124.012 130.758 124.179 130.425 124.179 129.925L125.178 130.259ZM123.013 129.925C123.346 130.592 123.346 130.758 123.513 131.424L122.513 131.757L122.014 130.259L123.013 129.925ZM126.51 134.256V133.256L126.677 129.759V128.926H127.843V130.592C128.842 130.925 129.675 131.424 130.508 131.924L129.841 132.923C129.342 132.59 128.676 132.091 128.009 131.757C127.843 131.757 127.843 131.757 127.843 131.591V134.089L126.51 134.256ZM129.342 129.259C129.508 129.592 129.675 129.759 129.841 130.092L129.342 130.425C129.175 130.092 129.009 129.759 128.842 129.592L129.342 129.259ZM130.175 128.926C130.341 129.093 130.508 129.426 130.841 129.759L130.341 130.092C130.175 129.759 130.008 129.426 129.841 129.259L130.175 128.926Z" fill="#E7F9E4"/>
|
|
||||||
<path d="M104.193 139.086C104.026 139.086 104.026 139.086 103.86 139.252H103.526C103.193 139.252 102.86 139.086 102.694 138.919C102.527 138.753 102.361 138.42 102.361 137.92C102.361 137.42 102.527 137.087 102.694 136.921C102.86 136.754 103.193 136.587 103.526 136.587H103.86C104.026 136.587 104.026 136.587 104.193 136.754V137.254C104.026 137.087 104.026 137.087 103.86 137.087H103.526C103.36 137.087 103.193 137.087 103.027 137.254C103.193 137.254 103.193 137.42 103.193 137.753C103.193 138.086 103.193 138.253 103.36 138.419C103.526 138.586 103.526 138.586 103.86 138.586H104.193C104.359 138.586 104.359 138.419 104.526 138.419L104.193 139.086ZM105.692 136.587H106.191V138.253H106.524V138.753H106.191V139.252H105.692V138.753H104.692V138.253L105.692 136.587ZM105.692 137.087L105.025 138.086H105.692V137.087ZM107.024 136.587H108.523V137.087H107.524V137.587H107.857C108.19 137.587 108.356 137.753 108.523 137.92C108.689 138.086 108.689 138.253 108.689 138.586C108.689 138.919 108.523 139.086 108.356 139.252C108.19 139.419 107.857 139.419 107.69 139.419H107.357C107.191 139.419 107.191 139.419 107.024 139.252V138.753C107.191 138.753 107.191 138.919 107.357 138.919H107.69C107.857 138.919 108.023 138.919 108.19 138.753C108.356 138.753 108.356 138.586 108.356 138.419C108.356 138.253 108.356 138.086 108.19 138.086C108.023 137.92 108.023 137.92 107.857 137.92H107.524C107.357 137.92 107.357 137.92 107.191 138.086L107.024 136.587ZM109.189 136.587H110.855V136.921L109.855 139.252H109.356L110.188 137.087H109.023L109.189 136.587ZM111.188 137.92C111.188 137.42 111.354 137.087 111.354 136.921C111.521 136.754 111.687 136.587 112.02 136.587C112.354 136.587 112.52 136.754 112.687 136.921C112.853 137.087 112.853 137.42 112.853 137.92C112.853 138.42 112.687 138.753 112.687 138.919C112.52 139.086 112.354 139.252 112.02 139.252C111.687 139.252 111.521 139.086 111.354 138.919C111.188 138.753 111.188 138.42 111.188 137.92ZM112.187 137.087C112.02 137.087 112.02 137.087 111.854 137.254C111.854 137.42 111.687 137.587 111.687 137.92V138.253L112.354 137.254C112.354 137.087 112.354 137.087 112.187 137.087ZM111.854 138.753C111.854 138.919 112.02 138.919 112.187 138.919C112.354 138.919 112.354 138.919 112.52 138.753C112.52 138.586 112.687 138.42 112.687 138.086V137.753L111.854 138.753ZM113.519 136.754H114.186C114.519 136.754 114.852 136.754 115.018 136.921C115.185 137.087 115.185 137.254 115.185 137.587C115.185 137.92 115.018 138.086 114.852 138.253C114.685 138.419 114.352 138.419 114.019 138.419H113.852V139.419H113.353L113.519 136.754ZM114.019 137.254V138.086H114.519C114.519 138.086 114.685 137.92 114.685 137.753C114.685 137.587 114.685 137.587 114.519 137.42C114.519 137.42 114.352 137.254 114.186 137.254H114.019ZM115.684 138.086C115.684 137.587 115.851 137.254 115.851 137.087C116.018 136.921 116.184 136.754 116.517 136.754C116.85 136.754 117.017 136.921 117.183 137.087C117.35 137.254 117.35 137.587 117.35 138.086C117.35 138.586 117.183 138.919 117.183 139.086C117.017 139.252 116.85 139.419 116.517 139.419C116.184 139.419 116.018 139.252 115.851 139.086C115.684 138.919 115.684 138.586 115.684 138.086ZM116.517 137.254C116.351 137.254 116.351 137.254 116.184 137.42C116.184 137.587 116.018 137.753 116.018 138.086V138.419L116.684 137.42C116.684 137.254 116.684 137.254 116.517 137.254ZM116.184 138.753C116.184 138.919 116.351 138.919 116.517 138.919C116.684 138.919 116.684 138.919 116.85 138.753C116.85 138.586 117.017 138.42 117.017 138.086V137.753L116.184 138.753ZM117.85 136.754H118.349C118.849 136.754 119.015 136.921 119.349 137.087C119.515 137.254 119.682 137.587 119.682 138.086C119.682 138.586 119.515 138.919 119.349 139.086C119.182 139.252 118.849 139.419 118.349 139.419H117.85V136.754ZM118.349 137.254V138.919H118.516C118.682 138.919 118.849 138.919 119.015 138.753C119.182 138.586 119.182 138.42 119.182 138.086C119.182 137.753 119.182 137.587 119.015 137.42C118.849 137.42 118.682 137.254 118.349 137.254Z" fill="#E7F9E4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,40 +0,0 @@
|
||||||
import "@github/markdown-toolbar-element";
|
|
||||||
import "@github/relative-time-element";
|
|
||||||
import "./modules/audio-clipper";
|
|
||||||
import ClientTimezone from "./modules/ClientTimezone";
|
|
||||||
import Clipboard from "./modules/Clipboard";
|
|
||||||
import DateTimePicker from "./modules/DateTimePicker";
|
|
||||||
import Dropdown from "./modules/Dropdown";
|
|
||||||
import HotKeys from "./modules/HotKeys";
|
|
||||||
import "./modules/markdown-preview";
|
|
||||||
import "./modules/markdown-write-preview";
|
|
||||||
import MultiSelect from "./modules/MultiSelect";
|
|
||||||
import "./modules/permalink-edit";
|
|
||||||
import "./modules/play-soundbite";
|
|
||||||
import PublishMessageWarning from "./modules/PublishMessageWarning";
|
|
||||||
import Select from "./modules/Select";
|
|
||||||
import SidebarToggler from "./modules/SidebarToggler";
|
|
||||||
import Slugify from "./modules/Slugify";
|
|
||||||
import ThemePicker from "./modules/ThemePicker";
|
|
||||||
import Time from "./modules/Time";
|
|
||||||
import Tooltip from "./modules/Tooltip";
|
|
||||||
import ValidateFileSize from "./modules/ValidateFileSize";
|
|
||||||
import "./modules/video-clip-previewer";
|
|
||||||
import VideoClipBuilder from "./modules/VideoClipBuilder";
|
|
||||||
import "./modules/xml-editor";
|
|
||||||
|
|
||||||
Dropdown();
|
|
||||||
Tooltip();
|
|
||||||
Select();
|
|
||||||
MultiSelect();
|
|
||||||
Slugify();
|
|
||||||
SidebarToggler();
|
|
||||||
ClientTimezone();
|
|
||||||
DateTimePicker();
|
|
||||||
Time();
|
|
||||||
Clipboard();
|
|
||||||
ThemePicker();
|
|
||||||
PublishMessageWarning();
|
|
||||||
HotKeys();
|
|
||||||
ValidateFileSize();
|
|
||||||
VideoClipBuilder();
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import Dropdown from "./modules/Dropdown";
|
|
||||||
import Tooltip from "./modules/Tooltip";
|
|
||||||
|
|
||||||
Dropdown();
|
|
||||||
Tooltip();
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import DrawCharts from "./modules/Charts";
|
|
||||||
|
|
||||||
DrawCharts();
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import Tooltip from "./modules/Tooltip";
|
|
||||||
|
|
||||||
Tooltip();
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import DrawEpisodesMaps from "./modules/EpisodesMap";
|
|
||||||
|
|
||||||
DrawEpisodesMaps();
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
import { indentWithTab } from "@codemirror/commands";
|
|
||||||
import { xml } from "@codemirror/lang-xml";
|
|
||||||
import {
|
|
||||||
defaultHighlightStyle,
|
|
||||||
syntaxHighlighting,
|
|
||||||
} from "@codemirror/language";
|
|
||||||
import { Compartment, EditorState } from "@codemirror/state";
|
|
||||||
import { keymap } from "@codemirror/view";
|
|
||||||
import { basicSetup, EditorView } from "codemirror";
|
|
||||||
import { css, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, queryAssignedNodes, state } from "lit/decorators.js";
|
|
||||||
import prettifyXML from "xml-formatter";
|
|
||||||
|
|
||||||
const language = new Compartment();
|
|
||||||
|
|
||||||
@customElement("xml-editor")
|
|
||||||
export class XMLEditor extends LitElement {
|
|
||||||
@queryAssignedNodes({ slot: "textarea" })
|
|
||||||
_textarea!: NodeListOf<HTMLTextAreaElement>;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
editorState!: EditorState;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
editorView!: EditorView;
|
|
||||||
|
|
||||||
firstUpdated(): void {
|
|
||||||
const minHeightEditor = EditorView.theme({
|
|
||||||
".cm-content, .cm-gutter": {
|
|
||||||
minHeight: this._textarea[0].clientHeight + "px",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let editorContents = "";
|
|
||||||
if (this._textarea[0].value) {
|
|
||||||
try {
|
|
||||||
editorContents = prettifyXML(this._textarea[0].value, {
|
|
||||||
indentation: " ",
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// xml doesn't have a root node
|
|
||||||
editorContents = prettifyXML(
|
|
||||||
"<root>" + this._textarea[0].value + "</root>",
|
|
||||||
{
|
|
||||||
indentation: " ",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// remove root, unnecessary lines and indents
|
|
||||||
editorContents = editorContents
|
|
||||||
.replace(/^<root>/, "")
|
|
||||||
.replace(/<\/root>$/, "")
|
|
||||||
.replace(/^\s*[\r\n]/gm, "")
|
|
||||||
.replace(/[\r\n] {2}/gm, "\r\n")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editorState = EditorState.create({
|
|
||||||
doc: editorContents,
|
|
||||||
extensions: [
|
|
||||||
basicSetup,
|
|
||||||
keymap.of([indentWithTab]),
|
|
||||||
language.of(xml()),
|
|
||||||
minHeightEditor,
|
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.editorView = new EditorView({
|
|
||||||
state: this.editorState,
|
|
||||||
root: this.shadowRoot as ShadowRoot,
|
|
||||||
parent: this.shadowRoot as ShadowRoot,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._textarea[0].hidden = true;
|
|
||||||
if (this._textarea[0].form) {
|
|
||||||
this._textarea[0].form.addEventListener("submit", () => {
|
|
||||||
this._textarea[0].value = this.editorView.state.doc.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
|
||||||
if (this._textarea[0].form) {
|
|
||||||
this._textarea[0].form.removeEventListener("submit", () => {
|
|
||||||
this._textarea[0].value = this.editorView.state.doc.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
.cm-editor {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 3px solid hsl(var(--color-border-contrast));
|
|
||||||
background-color: hsl(var(--color-background-elevated));
|
|
||||||
}
|
|
||||||
.cm-editor.cm-focused {
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 2px hsl(var(--color-background-elevated)),
|
|
||||||
0 0 0 calc(4px) hsl(var(--color-accent-base));
|
|
||||||
}
|
|
||||||
.cm-gutters {
|
|
||||||
background-color: hsl(var(--color-background-elevated)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-activeLine {
|
|
||||||
background-color: hsl(var(--color-background-highlight)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-activeLineGutter {
|
|
||||||
background-color: hsl(var(--color-background-highlight)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ͼ4 .cm-line {
|
|
||||||
caret-color: hsl(var(--color-text-base)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ͼ1 .cm-cursor {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
render(): TemplateResult<1> {
|
|
||||||
return html`<slot name="textarea"></slot>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import "@github/relative-time-element";
|
|
||||||
import SidebarToggler from "./modules/SidebarToggler";
|
|
||||||
import Time from "./modules/Time";
|
|
||||||
import Toggler from "./modules/Toggler";
|
|
||||||
import Tooltip from "./modules/Tooltip";
|
|
||||||
|
|
||||||
Time();
|
|
||||||
Toggler();
|
|
||||||
Tooltip();
|
|
||||||
SidebarToggler();
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
@import url("./tailwind.css");
|
|
||||||
@import url("./custom.css");
|
|
||||||
@import url("./fonts.css");
|
|
||||||
@import url("./colors.css");
|
|
||||||
@import url("./breadcrumb.css");
|
|
||||||
@import url("./dropdown.css");
|
|
||||||
@import url("./choices.css");
|
|
||||||
@import url("./radioBtn.css");
|
|
||||||
@import url("./colorRadioBtn.css");
|
|
||||||
@import url("./switch.css");
|
|
||||||
@import url("./radioToggler.css");
|
|
||||||
@import url("./formInputTabs.css");
|
|
||||||
@import url("./stickyHeader.css");
|
|
||||||
@import url("./readMore.css");
|
|
||||||
@import url("./seeMore.css");
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
@layer components {
|
|
||||||
.form-radio-btn {
|
|
||||||
@apply absolute mt-3 ml-3 border-contrast border-3 text-accent-base;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
@apply ring-accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
@apply ring-2 ring-contrast;
|
|
||||||
|
|
||||||
& + label {
|
|
||||||
@apply text-accent-contrast bg-accent-base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& + label {
|
|
||||||
@apply inline-flex items-center py-2 pl-8 pr-2 text-sm font-semibold rounded-lg cursor-pointer border-contrast bg-elevated border-3;
|
|
||||||
|
|
||||||
color: hsl(var(--color-text-muted));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,13 +11,15 @@ declare(strict_types=1);
|
||||||
namespace App\Validation;
|
namespace App\Validation;
|
||||||
|
|
||||||
use CodeIgniter\Validation\FileRules as ValidationFileRules;
|
use CodeIgniter\Validation\FileRules as ValidationFileRules;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class FileRules extends ValidationFileRules
|
class FileRules extends ValidationFileRules
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Checks an uploaded file to verify that the dimensions are within a specified allowable dimension.
|
* 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
|
// Grab the file name off the top of the $params
|
||||||
// after we split it.
|
// 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
|
* 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
|
// Grab the file name off the top of the $params
|
||||||
// after we split it.
|
// after we split it.
|
||||||
|
|
@ -99,7 +101,7 @@ class FileRules extends ValidationFileRules
|
||||||
/**
|
/**
|
||||||
* Checks that an uploaded json file's content is valid
|
* 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
|
// Grab the file name off the top of the $params
|
||||||
// after we split it.
|
// 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,58 +4,57 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components;
|
namespace App\Views\Components;
|
||||||
|
|
||||||
|
use Override;
|
||||||
use ViewComponents\Component;
|
use ViewComponents\Component;
|
||||||
|
|
||||||
class Alert extends 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'
|
* @var 'default'|'success'|'danger'|'warning'
|
||||||
*/
|
*/
|
||||||
protected string $variant = 'default';
|
protected string $variant = 'default';
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$variants = [
|
$variantData = match ($this->variant) {
|
||||||
'success' => [
|
'success' => [
|
||||||
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
|
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||||
'glyph' => 'check-fill', // @icon('check-fill')
|
'glyph' => 'check-fill', // @icon("check-fill")
|
||||||
],
|
],
|
||||||
'danger' => [
|
'danger' => [
|
||||||
'class' => 'text-red-900 bg-red-100 border-red-300',
|
'class' => 'text-red-900 bg-red-100 border-red-300',
|
||||||
'glyph' => 'close-fill', // @icon('close-fill')
|
'glyph' => 'close-fill', // @icon("close-fill")
|
||||||
],
|
],
|
||||||
'warning' => [
|
'warning' => [
|
||||||
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||||
'glyph' => 'alert-fill', // @icon('alert-fill')
|
'glyph' => 'alert-fill', // @icon("alert-fill")
|
||||||
],
|
],
|
||||||
'default' => [
|
default => [
|
||||||
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
|
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
|
||||||
'glyph' => 'error-warning-fill', // @icon('error-warning-fill')
|
'glyph' => 'error-warning-fill', // @icon("error-warning-fill")
|
||||||
],
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
if (! array_key_exists($this->variant, $variants)) {
|
$glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [
|
||||||
$this->variant = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
$glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [
|
|
||||||
'class' => 'flex-shrink-0 mr-2 text-lg',
|
'class' => 'flex-shrink-0 mr-2 text-lg',
|
||||||
]);
|
]);
|
||||||
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
$title = $this->title === '' ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
||||||
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;
|
$this->mergeClass('inline-flex w-full p-2 text-sm border rounded ');
|
||||||
|
$this->mergeClass($variantData['class']);
|
||||||
unset($this->attributes['slot']);
|
|
||||||
unset($this->attributes['variant']);
|
|
||||||
unset($this->attributes['class']);
|
|
||||||
unset($this->attributes['glyph']);
|
|
||||||
$attributes = stringify_attributes($this->attributes);
|
|
||||||
|
|
||||||
return <<<HTML
|
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;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,25 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components;
|
namespace App\Views\Components;
|
||||||
|
|
||||||
|
use Override;
|
||||||
use ViewComponents\Component;
|
use ViewComponents\Component;
|
||||||
|
|
||||||
class Button extends 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 $uri = '';
|
||||||
|
|
||||||
protected string $variant = 'default';
|
protected string $variant = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var 'small'|'base'|'large'
|
||||||
|
*/
|
||||||
protected string $size = 'base';
|
protected string $size = 'base';
|
||||||
|
|
||||||
protected string $iconLeft = '';
|
protected string $iconLeft = '';
|
||||||
|
|
@ -20,65 +31,54 @@ class Button extends Component
|
||||||
|
|
||||||
protected bool $isSquared = false;
|
protected bool $isSquared = false;
|
||||||
|
|
||||||
public function setIsSquared(string $value): void
|
protected bool $isExternal = false;
|
||||||
{
|
|
||||||
$this->isSquared = $value === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$baseClass =
|
$this->mergeClass('shadow gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full');
|
||||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
|
|
||||||
|
|
||||||
$variantClass = [
|
$variantClass = match ($this->variant) {
|
||||||
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
'primary' => 'text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||||
'primary' => 'shadow-sm 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',
|
||||||
'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
'danger' => 'bg-red-50 ring-2 ring-red-700 ring-inset text-red-700 hover:ring-red-800 hover:text-red-800',
|
||||||
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
'warning' => 'bg-yellow-50 ring-2 ring-yellow-700 ring-inset text-yellow-700 hover:ring-yellow-800 hover:text-yellow-800',
|
||||||
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
'info' => 'bg-blue-50 ring-2 ring-blue-700 ring-inset text-blue-700 hover:ring-blue-800 hover:text-blue-800',
|
||||||
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
'disabled' => 'text-black bg-gray-300 cursor-not-allowed',
|
||||||
'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
|
default => 'text-black bg-gray-50 hover:bg-gray-200',
|
||||||
'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
|
};
|
||||||
];
|
|
||||||
|
|
||||||
$sizeClass = [
|
$sizeClass = match ($this->size) {
|
||||||
'small' => 'text-xs leading-6',
|
'small' => 'text-xs leading-6',
|
||||||
'base' => 'text-sm leading-5',
|
|
||||||
'large' => 'text-base leading-6',
|
'large' => 'text-base leading-6',
|
||||||
];
|
default => 'text-sm leading-5',
|
||||||
|
};
|
||||||
|
|
||||||
$iconSize = [
|
$iconSizeClass = match ($this->size) {
|
||||||
'small' => 'text-sm',
|
'small' => 'text-sm',
|
||||||
'base' => 'text-lg',
|
|
||||||
'large' => 'text-2xl',
|
'large' => 'text-2xl',
|
||||||
];
|
default => 'text-lg',
|
||||||
|
};
|
||||||
|
|
||||||
$basePaddings = [
|
$basePaddings = match ($this->size) {
|
||||||
'small' => 'px-3 py-1',
|
'small' => 'px-3 py-1',
|
||||||
'base' => 'px-3 py-2',
|
|
||||||
'large' => 'px-4 py-2',
|
'large' => 'px-4 py-2',
|
||||||
];
|
default => 'px-3 py-2',
|
||||||
|
};
|
||||||
|
|
||||||
$squaredPaddings = [
|
$squaredPaddings = match ($this->size) {
|
||||||
'small' => 'p-1',
|
'small' => 'p-1',
|
||||||
'base' => 'p-2',
|
|
||||||
'large' => 'p-3',
|
'large' => 'p-3',
|
||||||
];
|
default => 'p-2',
|
||||||
|
};
|
||||||
|
|
||||||
$buttonClass =
|
$this->mergeClass($variantClass);
|
||||||
$baseClass .
|
$this->mergeClass($sizeClass);
|
||||||
' ' .
|
|
||||||
($this->isSquared
|
|
||||||
? $squaredPaddings[$this->size]
|
|
||||||
: $basePaddings[$this->size]) .
|
|
||||||
' ' .
|
|
||||||
$sizeClass[$this->size] .
|
|
||||||
' ' .
|
|
||||||
$variantClass[$this->variant];
|
|
||||||
|
|
||||||
if (array_key_exists('class', $this->attributes)) {
|
if ($this->isSquared) {
|
||||||
$buttonClass .= ' ' . $this->attributes['class'];
|
$this->mergeClass($squaredPaddings);
|
||||||
unset($this->attributes['class']);
|
} else {
|
||||||
|
$this->mergeClass($basePaddings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->iconLeft !== '' || $this->iconRight !== '') {
|
if ($this->iconLeft !== '' || $this->iconRight !== '') {
|
||||||
|
|
@ -87,41 +87,30 @@ class Button extends Component
|
||||||
|
|
||||||
if ($this->iconLeft !== '') {
|
if ($this->iconLeft !== '') {
|
||||||
$this->slot = icon($this->iconLeft, [
|
$this->slot = icon($this->iconLeft, [
|
||||||
'class' => 'opacity-75 ' . $iconSize[$this->size],
|
'class' => 'opacity-75 ' . $iconSizeClass,
|
||||||
]) . $this->slot;
|
]) . $this->slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->iconRight !== '') {
|
if ($this->iconRight !== '') {
|
||||||
$this->slot .= icon($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 !== '') {
|
if ($this->uri !== '') {
|
||||||
$tagName = 'a';
|
$tagName = 'a';
|
||||||
$defaultButtonAttributes = [
|
$this->attributes['href'] = $this->uri;
|
||||||
'href' => $this->uri,
|
if ($this->isExternal) {
|
||||||
];
|
$this->attributes['target'] = '_blank';
|
||||||
|
$this->attributes['rel'] = 'noopener noreferrer';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$tagName = 'button';
|
$tagName = 'button';
|
||||||
$defaultButtonAttributes = [
|
$this->attributes['type'] ??= 'button';
|
||||||
'type' => 'button',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
|
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<{$tagName} class="{$buttonClass}" {$attributes}>{$this->slot}</{$tagName}>
|
<{$tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$tagName}>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,22 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components\Charts;
|
namespace App\Views\Components\Charts;
|
||||||
|
|
||||||
|
use Override;
|
||||||
use ViewComponents\Component;
|
use ViewComponents\Component;
|
||||||
|
|
||||||
class ChartsComponent extends Component
|
class ChartsComponent extends Component
|
||||||
{
|
{
|
||||||
protected string $title = '';
|
protected array $props = ['title', 'subtitle', 'dataUrl', 'type'];
|
||||||
|
|
||||||
|
protected string $title;
|
||||||
|
|
||||||
protected string $subtitle = '';
|
protected string $subtitle = '';
|
||||||
|
|
||||||
protected string $dataUrl = '';
|
protected string $dataUrl;
|
||||||
|
|
||||||
protected string $type = '';
|
protected string $type;
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$subtitleBlock = '';
|
$subtitleBlock = '';
|
||||||
|
|
@ -23,8 +27,10 @@ class ChartsComponent extends Component
|
||||||
$subtitleBlock = '<p class="px-6 -mt-4 text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
$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
|
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>
|
<h2 class="px-6 py-4 text-xl">{$this->title}</h2>
|
||||||
{$subtitleBlock}
|
{$subtitleBlock}
|
||||||
<div class="w-full h-[500px]" data-chart-type="{$this->type}" data-chart-url="{$this->dataUrl}"></div>
|
<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;
|
namespace App\Views\Components;
|
||||||
|
|
||||||
|
use Override;
|
||||||
use ViewComponents\Component;
|
use ViewComponents\Component;
|
||||||
|
|
||||||
class DashboardCard extends Component
|
class DashboardCard extends Component
|
||||||
{
|
{
|
||||||
protected ?string $href = null;
|
protected array $props = ['href', 'glyph', 'title', 'subtitle'];
|
||||||
|
|
||||||
|
protected string $href = '';
|
||||||
|
|
||||||
protected string $glyph;
|
protected string $glyph;
|
||||||
|
|
||||||
|
|
@ -21,17 +24,18 @@ class DashboardCard extends Component
|
||||||
$this->subtitle = html_entity_decode($value);
|
$this->subtitle = html_entity_decode($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$glyph = icon($this->glyph, [
|
$glyph = (string) icon($this->glyph, [
|
||||||
'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base',
|
'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($this->href !== null && $this->href !== '') {
|
if ($this->href !== '') {
|
||||||
$chevronRight = icon('arrow-right-s-fill');
|
$chevronRight = icon('arrow-right-s-fill');
|
||||||
$viewLang = lang('Common.view');
|
$viewLang = lang('Common.view');
|
||||||
return <<<HTML
|
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="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>
|
<div class="text-5xl font-bold">{$this->slot}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,37 @@ declare(strict_types=1);
|
||||||
namespace App\Views\Components;
|
namespace App\Views\Components;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Override;
|
||||||
use ViewComponents\Component;
|
use ViewComponents\Component;
|
||||||
|
|
||||||
class DropdownMenu extends 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
|
public function setItems(string $value): void
|
||||||
{
|
{
|
||||||
$this->items = json_decode(htmlspecialchars_decode($value), true);
|
$this->items = json_decode(htmlspecialchars_decode($value), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
if ($this->items === []) {
|
if ($this->items === []) {
|
||||||
|
|
@ -37,7 +47,7 @@ class DropdownMenu extends Component
|
||||||
switch ($item['type']) {
|
switch ($item['type']) {
|
||||||
case 'link':
|
case 'link':
|
||||||
$menuItems .= anchor($item['uri'], $item['title'], [
|
$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;
|
break;
|
||||||
case 'html':
|
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
|
return <<<HTML
|
||||||
<nav id="{$this->id}"
|
<nav {$this->getStringifiedAttributes()}>{$menuItems}</nav>
|
||||||
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>
|
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,59 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components\Forms;
|
namespace App\Views\Components\Forms;
|
||||||
|
|
||||||
|
use App\Views\Components\Hint;
|
||||||
|
use Override;
|
||||||
|
|
||||||
class Checkbox extends FormComponent
|
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
|
protected string $hint = '';
|
||||||
{
|
|
||||||
$this->isChecked = $value === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
protected string $helper = '';
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
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(
|
$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',
|
'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
|
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;
|
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;
|
namespace App\Views\Components\Forms;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class ColorRadioButton extends FormComponent
|
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
|
protected bool $isSelected = false;
|
||||||
{
|
|
||||||
$this->isChecked = $value === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
|
|
@ -23,18 +25,18 @@ class ColorRadioButton extends FormComponent
|
||||||
'class' => 'color-radio-btn',
|
'class' => 'color-radio-btn',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->required) {
|
if ($this->isRequired) {
|
||||||
$data['required'] = 'required';
|
$data['required'] = 'required';
|
||||||
}
|
}
|
||||||
|
|
||||||
$radioInput = form_radio(
|
$radioInput = form_radio(
|
||||||
$data,
|
$data,
|
||||||
$this->value,
|
$this->value,
|
||||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div class="{$this->class}" style="{$this->style}">
|
<div {$this->getStringifiedAttributes()}>
|
||||||
{$radioInput}
|
{$radioInput}
|
||||||
<label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
|
<label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,34 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components\Forms;
|
namespace App\Views\Components\Forms;
|
||||||
|
|
||||||
|
use Override;
|
||||||
|
|
||||||
class DatetimePicker extends FormComponent
|
class DatetimePicker extends FormComponent
|
||||||
{
|
{
|
||||||
|
protected array $attributes = [
|
||||||
|
'data-picker' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$this->attributes['class'] = 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0';
|
$dateInput = form_input([
|
||||||
$this->attributes['data-input'] = '';
|
'name' => $this->name,
|
||||||
$dateInput = form_input($this->attributes, old($this->name, $this->value));
|
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
|
||||||
|
'data-input' => '',
|
||||||
|
], $this->getValue());
|
||||||
|
|
||||||
$clearLabel = lang(
|
$clearLabel = lang(
|
||||||
'Episode.publish_form.scheduled_publication_date_clear',
|
'Episode.publish_form.scheduled_publication_date_clear',
|
||||||
);
|
);
|
||||||
$closeIcon = icon('close-fill');
|
$closeIcon = icon('close-fill');
|
||||||
|
|
||||||
|
$this->mergeClass('flex border-3 rounded-lg border-contrast focus-within:ring-accent transition');
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div class="flex border-3 rounded-lg border-contrast focus-within:ring-accent {$this->class}" data-picker="datetime">
|
<div {$this->getStringifiedAttributes()}>
|
||||||
{$dateInput}
|
{$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}
|
{$closeIcon}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,51 +4,80 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Views\Components\Forms;
|
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 $as = 'Input';
|
||||||
|
|
||||||
protected string $label = '';
|
protected string $hint = '';
|
||||||
|
|
||||||
protected ?string $helper = null;
|
protected string $helper = '';
|
||||||
|
|
||||||
protected ?string $hint = null;
|
|
||||||
|
|
||||||
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$helperText = '';
|
$helperText = '';
|
||||||
if ($this->helper !== null) {
|
if ($this->helper !== '') {
|
||||||
$helperId = $this->id . 'Help';
|
$helperId = $this->name . 'Help';
|
||||||
$helperText = '<Forms.Helper id="' . $helperId . '">' . $this->helper . '</Forms.Helper>';
|
$helperText = new Helper([
|
||||||
|
'id' => $helperId,
|
||||||
|
'slot' => $this->helper,
|
||||||
|
])->render();
|
||||||
$this->attributes['aria-describedby'] = $helperId;
|
$this->attributes['aria-describedby'] = $helperId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$labelAttributes = [
|
$labelAttributes = [
|
||||||
'for' => $this->id,
|
'for' => $this->name,
|
||||||
'isOptional' => $this->required ? 'false' : 'true',
|
'isOptional' => $this->isRequired ? 'false' : 'true',
|
||||||
'class' => '-mb-1',
|
'class' => '-mb-1',
|
||||||
|
'slot' => $this->label,
|
||||||
];
|
];
|
||||||
if ($this->hint) {
|
if ($this->hint !== '') {
|
||||||
$labelAttributes['hint'] = $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
|
$this->mergeClass('flex flex-col');
|
||||||
$fieldComponentAttributes = $this->attributes;
|
$fieldClass = $this->attributes['class'];
|
||||||
unset($fieldComponentAttributes['as']);
|
|
||||||
unset($fieldComponentAttributes['label']);
|
|
||||||
unset($fieldComponentAttributes['class']);
|
|
||||||
unset($fieldComponentAttributes['helper']);
|
|
||||||
unset($fieldComponentAttributes['hint']);
|
|
||||||
|
|
||||||
|
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;
|
$element = __NAMESPACE__ . '\\' . $this->as;
|
||||||
$fieldElement = new $element($fieldComponentAttributes);
|
$fieldElement = new $element($this->attributes);
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div class="flex flex-col {$this->class}">
|
<div class="{$fieldClass}">
|
||||||
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
|
{$label->render()}
|
||||||
{$helperText}
|
{$helperText}
|
||||||
<div class="w-full mt-1">
|
<div class="relative w-full mt-1">
|
||||||
{$fieldElement->render()}
|
{$fieldElement->render()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
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