|
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 466 KiB |
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 926 KiB After Width: | Height: | Size: 926 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 642 KiB After Width: | Height: | Size: 642 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 529 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 590 KiB After Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 872 KiB After Width: | Height: | Size: 872 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
|
@ -2,6 +2,10 @@ Title: Actualités
|
|||
|
||||
----
|
||||
|
||||
Mentions: art, design, recherche
|
||||
|
||||
----
|
||||
|
||||
Categories: Dans les murs, Hors les murs
|
||||
|
||||
----
|
||||
|
|
@ -13,20 +13,21 @@ tabs:
|
|||
- width: 1/2
|
||||
fields:
|
||||
category:
|
||||
label: Categorie
|
||||
label: Types
|
||||
type: select
|
||||
options: query
|
||||
query: page.parent.categories.split
|
||||
width: 1/2
|
||||
help: |
|
||||
<a href="/panel/pages/news?tab=indexation">Administrer la liste</a>
|
||||
<a href="/panel/pages/actualites?tab=indexation">Administrer la liste</a>
|
||||
tags:
|
||||
label: Mots-clés
|
||||
type: multiselect
|
||||
options: query
|
||||
query: page.parent.tags.split
|
||||
width: 1/2
|
||||
help: |
|
||||
<a href="/panel/pages/news?tab=indexation">Administrer la liste</a>
|
||||
<a href="/panel/pages/actualites?tab=indexation">Administrer la liste</a>
|
||||
cover:
|
||||
label: Image de couverture
|
||||
type: files
|
||||
|
|
|
|||
|
|
@ -20,12 +20,17 @@ tabs:
|
|||
query: page.cover.toFile
|
||||
indexation:
|
||||
icon: folder-structure
|
||||
fields:
|
||||
categories:
|
||||
type: tags
|
||||
width: 1/2
|
||||
tags:
|
||||
type: tags
|
||||
width: 1/2
|
||||
columns:
|
||||
- width: 1/2
|
||||
fields:
|
||||
mentions:
|
||||
type: tags
|
||||
disabled: true
|
||||
categories:
|
||||
type: tags
|
||||
- width: 1/2
|
||||
fields:
|
||||
tags:
|
||||
type: tags
|
||||
files: tabs/files
|
||||
seo: seo/page
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.php]
|
||||
indent_size = 2
|
||||
10
site/plugins/promote-button/.gitignore
vendored
|
|
@ -1,10 +0,0 @@
|
|||
# System files
|
||||
# ---------------------
|
||||
.DS_Store
|
||||
node_modules
|
||||
package-lock.json
|
||||
Icon
|
||||
|
||||
# Lock Files
|
||||
# ---------------------
|
||||
.lock
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Scott Boms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
# Promote Button for Kirby
|
||||
|
||||

|
||||
|
||||
A customizable View Button for Kirby 5 that builds on Bastian's demo from the [Kirby 5 Release Show](https://youtube.com/watch?v=o2xkzqiLEUM) adding missing functionality and configuration settings for Mastodon, Bluesky, and LinkedIn as well as other user-experience enhancements.
|
||||
|
||||
## Requirements
|
||||
|
||||
This plugin requires Kirby 5.x and newer. It will not work with earlier versions of Kirby.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### [Kirby CLI](https://github.com/getkirby/cli)
|
||||
|
||||
kirby plugin:install scottboms/kirby-promote-button
|
||||
|
||||
### Git submodule
|
||||
|
||||
git submodule add https://github.com/scottboms/kirby-promote-button.git site/plugins/promote-button
|
||||
|
||||
### Copy and Paste
|
||||
|
||||
1. [Download](https://github.com/scottboms/kirby-promote-button/archive/master.zip) the contents of this repository as Zip file.
|
||||
2. Rename the extracted folder to `promote-button` and copy it into the `site/plugins/` directory in your project.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
To function, the plugin requires configuration as outlined below.
|
||||
|
||||
### Required Settings
|
||||
|
||||
Add these settings to your `/site/config/config.php` or `/site/config/env.php` file. Define which services you wish to use and then replace the CAPITALIZED PLACEHOLDERS with the necessary values.
|
||||
|
||||
#### General
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'scottboms.promote' => [
|
||||
'services' => [
|
||||
'mastodon',
|
||||
'bluesky',
|
||||
'linkedin'
|
||||
],
|
||||
'mastodon' => [
|
||||
'username' => 'USERNAME', // e.g. scottboms
|
||||
'url' => 'MASTODON_HOST', // e.g. mastodon.social
|
||||
],
|
||||
'bluesky' => [
|
||||
'base_url' => 'BLUESKY_HOST', // e.g. bsky.social
|
||||
'handle' => 'USERNAME', // e.g. example.bsky.social
|
||||
]
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
#### Tokens and Passwords
|
||||
|
||||
To post to [Mastodon](https://mastodon.social/settings/applications), [Bluesky](https://bsky.app/settings/app-passwords) or [LinkedIn](https://linkedin.com/developers/apps), you will need the necessary authentication tokens or app passwords. Because this information is sensitive, you should not include these settings in your `/site/config/config.php` file and instead place them in the [env.php config file](https://getkirby.com/docs/guide/configuration#multi-environment-setup__deployment-configuration) which should be added to a `.gitignore` file to avoid sharing this info publicly.
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'scottboms.promote.mastodon.token' => 'MASTODON_API_TOKEN',
|
||||
'scottboms.promote.bluesky.password' => 'BLUESKY_APP_PASSWORD',
|
||||
'scottboms.promote.linkedin.token' => 'LINKEDIN_OAUTH_TOKEN',
|
||||
],
|
||||
```
|
||||
|
||||
|
||||
### Optional Settings
|
||||
|
||||
If you run your Kirby site locally, the Promote button will function but page urls added to the dialog will use the local hostname (e.g. localhost) which isn't very helpful when posting to public services. You can override this behaviour by setting `host_url` in the configuration.
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'scottboms.promote' => [
|
||||
'host_url' => 'SHARED_LINK_HOST', // e.g. https://example.com
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Blueprint Configuration
|
||||
|
||||
There are multiple methods to add [View Buttons](https://getkirby.com/releases/5/view-buttons) to your Kirby installation. This plugin includes two distinct View Buttons -- the Promote button to access the core features of this plugin, and the Profile button which currently allows a way to quickly go to a Mastodon profile. The buttons can be added to any page by adding the `buttons` [option](https://getkirby.com/docs/reference/panel/blueprints/page#view-buttons) in a Page or Site Blueprint.
|
||||
|
||||
```yml
|
||||
buttons:
|
||||
promote: true
|
||||
profile: true
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
* Original Concept and Starting Points: [Bastian Allgeier](https://github.com/bastianallgeier/)
|
||||
* Supported Services: [Mastodon](https://mastodon.social), [Bluesky](https://bsky.app), [LinkedIn](https://linkedin.com)
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This plugin is provided "as is" with no guarantee. Use it at your own risk and always test before using it in a production environment. If you identify an issue, typo, etc, please [create a new issue](/issues/new) so I can investigate.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace ScottBoms\Promote;
|
||||
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Exception;
|
||||
|
||||
class PlatformPromoter
|
||||
{
|
||||
protected $config;
|
||||
protected const USER_AGENT = 'KirbyPromote/1.0 (+https://scottboms.com)';
|
||||
protected bool $cacheEnabled = true;
|
||||
protected int $cacheTimeout = 3600; // 1 hour in seconds
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = [
|
||||
'mastodon' => [
|
||||
'token' => option('scottboms.promote.mastodon.token'),
|
||||
'username' => option('scottboms.promote.mastodon.username'),
|
||||
'url' => option('scottboms.promote.mastodon.url', 'mastodon.social'),
|
||||
],
|
||||
'linkedin' => [
|
||||
'username' => option('scottboms.promote.linkedin.username'),
|
||||
'token' => option('scottboms.promote.linkedin.token'),
|
||||
],
|
||||
'bluesky' => [
|
||||
'base_url' => option('scottboms.promote.bluesky.base_url', 'bsky.social'),
|
||||
'handle' => option('scottboms.promote.bluesky.handle'),
|
||||
'password' => option('scottboms.promote.bluesky.password'),
|
||||
],
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Posting...
|
||||
public function post(string $platform, string $text): void
|
||||
{
|
||||
if (!method_exists($this, $platform)) {
|
||||
throw new Exception("Platform '$platform' is not supported.");
|
||||
}
|
||||
|
||||
$this->{$platform}($text);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Mastodon
|
||||
protected function mastodon(string $text): void
|
||||
{
|
||||
$text = trim($text); // remove whitespace
|
||||
|
||||
if ($text === '') {
|
||||
// $this->log('mastodon', 'error', 'Aborted: status text is empty');
|
||||
throw new Exception('Mastodon post failed: status text is empty');
|
||||
}
|
||||
|
||||
$token = $this->config['mastodon']['token'];
|
||||
$baseUrl = 'https://' . rtrim($this->config['mastodon']['url'], '/');
|
||||
$url = $baseUrl . '/api/v1/statuses';
|
||||
|
||||
// $this->log('mastodon', 'debug', 'Posting status: ' . $text);
|
||||
|
||||
// Ensure proper encoding
|
||||
$text = mb_convert_encoding($text, 'UTF-8', 'auto');
|
||||
$idempotencyKey = hash('sha256', $text); // hash of the content
|
||||
|
||||
$payloadArray = [
|
||||
'status' => $text,
|
||||
'language' => 'en',
|
||||
'visibility' => 'public'
|
||||
];
|
||||
|
||||
$payload = json_encode($payloadArray);
|
||||
|
||||
if ($payload === false) {
|
||||
// $this->log('mastodon', 'error', 'Failed to encode payload: ' . json_last_error_msg());
|
||||
throw new Exception('Mastodon post failed: JSON encoding error');
|
||||
}
|
||||
|
||||
$this->log('mastodon', 'debug', 'Final payload: ' . $payload);
|
||||
|
||||
// Use cURL to post to Mastodon
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
'Idempotency-Key: ' . $idempotencyKey,
|
||||
'User-Agent: ' . self::USER_AGENT,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
$this->log('mastodon', 'debug', 'Response HTTP code: ' . $httpCode);
|
||||
// $this->log('mastodon', 'debug', 'Response: ' . $response);
|
||||
|
||||
if (!in_array($httpCode, [200, 202])) {
|
||||
throw new Exception('Mastodon post failed with code ' . $httpCode . ': ' . $response);
|
||||
}
|
||||
|
||||
$this->log('mastodon', 'info', 'Post succeeded.');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Bluesky
|
||||
protected function bluesky(string $text): void
|
||||
{
|
||||
// Step 1: Authenticate
|
||||
$identifier = preg_replace('/[^\x21-\x7E]/', '', ltrim($this->config['bluesky']['handle'], '@'));
|
||||
$password = $this->config['bluesky']['password'];
|
||||
$baseUrl = 'https://' . rtrim($this->config['bluesky']['base_url'] ?? 'https://bsky.social', '/');
|
||||
|
||||
$payload = json_encode([
|
||||
'identifier' => $identifier,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
// Use native cURL, json_encode/decode seemed to fail
|
||||
$ch = curl_init($baseUrl . '/xrpc/com.atproto.server.createSession');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
'User-Agent: ' . self::USER_AGENT
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
$this->log('bluesky', 'debug', "cURL auth code: $httpCode");
|
||||
// $this->log('bluesky', 'debug', "cURL auth response: $response");
|
||||
|
||||
$authData = json_decode($response, true);
|
||||
|
||||
if ($httpCode !== 200 || !isset($authData['accessJwt'])) {
|
||||
throw new Exception('Bluesky authentication failed: ' . $response);
|
||||
}
|
||||
|
||||
$accessJwt = $authData['accessJwt'];
|
||||
$did = $authData['did'];
|
||||
|
||||
// Step 2: Post
|
||||
$postPayload = json_encode([
|
||||
'collection' => 'app.bsky.feed.post',
|
||||
'repo' => $did,
|
||||
'record' => [
|
||||
'$type' => 'app.bsky.feed.post',
|
||||
'text' => $text,
|
||||
'langs' => ['en-US'], // must be array
|
||||
'createdAt' => date('c'),
|
||||
]
|
||||
]);
|
||||
|
||||
$ch = curl_init($baseUrl . '/xrpc/com.atproto.repo.createRecord');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postPayload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $accessJwt,
|
||||
'Content-Length: ' . strlen($postPayload),
|
||||
'User-Agent: ' . self::USER_AGENT
|
||||
]);
|
||||
|
||||
$postResponse = curl_exec($ch);
|
||||
$postHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
$this->log('bluesky', 'debug', 'Post HTTP code: ' . $postHttpCode);
|
||||
// $this->log('bluesky', 'debug', 'Post response: ' . $postResponse);
|
||||
|
||||
if ($postHttpCode !== 200) {
|
||||
throw new Exception('Bluesky post failed: ' . $postResponse);
|
||||
}
|
||||
|
||||
$this->log('bluesky', 'info', 'Post succeeded.');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// LinkedIn
|
||||
protected function getLinkedInAuthorUrn(): string
|
||||
{
|
||||
$cache = kirby()->cache('linkedin');
|
||||
$cacheKey = 'author_urn';
|
||||
|
||||
// Check cache first
|
||||
if($this->cacheEnabled) {
|
||||
$cachedUrn = $cache->get($cacheKey);
|
||||
if($cachedUrn !== null) {
|
||||
$this->log('linked', 'debug', 'Using cached LinkedIn author URN');
|
||||
return $cachedUrn;
|
||||
}
|
||||
}
|
||||
|
||||
$token = $this->config['linkedin']['token'];
|
||||
|
||||
$response = Remote::get('https://api.linkedin.com/v2/userinfo', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->code() !== 200) {
|
||||
throw new Exception('LinkedIn /userinfo failed: ' . $response->content());
|
||||
}
|
||||
|
||||
$data = json_decode($response->content(), true);
|
||||
if (!isset($data['sub'])) {
|
||||
throw new \Exception('LinkedIn /userinfo response missing "sub" field.');
|
||||
}
|
||||
|
||||
$urn = 'urn:li:person:' . $data['sub'];
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
$cache->set($cacheKey, $urn, $this->cacheTimeout);
|
||||
$this->log('linkedin', 'debug', 'Cached LinkedIn author URN');
|
||||
}
|
||||
|
||||
return $urn;
|
||||
}
|
||||
|
||||
|
||||
protected function linkedin(string $text): void
|
||||
{
|
||||
$token = $this->config['linkedin']['token'];
|
||||
$author = $this->getLinkedInAuthorUrn();
|
||||
|
||||
$payload = [
|
||||
'author' => $author,
|
||||
'lifecycleState' => 'PUBLISHED',
|
||||
'specificContent' => [
|
||||
'com.linkedin.ugc.ShareContent' => [
|
||||
'shareCommentary' => [
|
||||
'text' => $text
|
||||
],
|
||||
'shareMediaCategory' => 'NONE'
|
||||
]
|
||||
],
|
||||
'visibility' => [
|
||||
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC'
|
||||
]
|
||||
];
|
||||
|
||||
// $this->log('linkedin', 'debug', 'Post JSON: ' . json_encode($payload));
|
||||
|
||||
$json = json_encode($payload);
|
||||
if ($json === false) {
|
||||
$this->log('linkedin', 'error', 'Failed to encode payload: ' . json_last_error_msg());
|
||||
throw new Exception('LinkedIn post failed: JSON encoding error');
|
||||
}
|
||||
|
||||
$ch = curl_init('https://api.linkedin.com/v2/ugcPosts');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
'X-Restli-Protocol-Version: 2.0.0',
|
||||
'Content-Length: ' . strlen($json),
|
||||
'User-Agent: ' . self::USER_AGENT
|
||||
]);
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
$responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$this->log('linkedin', 'debug', 'Response Code: ' . $responseCode);
|
||||
// $this->log('linkedin', 'debug', 'Response Body: ' . $responseBody);
|
||||
|
||||
if ($responseCode !== 201) {
|
||||
throw new Exception('LinkedIn post failed: ' . $responseBody);
|
||||
}
|
||||
|
||||
$this->log('linkedin', 'info', 'Post succeeded.');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Logging
|
||||
// Implements custom logging to /site/logs/promote.log
|
||||
private function log(string $platform, string $level, string $message): void
|
||||
{
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logDir = kirby()->root('logs');
|
||||
$logFile = $logDir . '/promote.log';
|
||||
|
||||
// Ensure log directory exists
|
||||
Dir::make($logDir);
|
||||
$entry = Str::unhtml("[$timestamp][$level][$platform] $message") . PHP_EOL;
|
||||
F::append($logFile, $entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"name": "scottboms/promote-button",
|
||||
"description": "Promote Panel Button for Kirby.",
|
||||
"type": "kirby-plugin",
|
||||
"homepage": "https://github.com/scottboms/kirby-promote-button",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Scott Boms",
|
||||
"email": "plugins@scottboms.com",
|
||||
"homepage": "https://scottboms.com"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://github.com/scottboms/kirby-promote-button/blob/main/README.md",
|
||||
"source": "https://github.com/scottboms/kirby-promote-button"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"kirby",
|
||||
"kirby5",
|
||||
"kirby-cms",
|
||||
"kirby-plugin",
|
||||
"kirby5-plugin"
|
||||
],
|
||||
"require": {
|
||||
"php": ">8.1.0 <8.4.0",
|
||||
"getkirby/cms": "^5.0",
|
||||
"getkirby/composer-installer": "^1.1"
|
||||
},
|
||||
"extra": {
|
||||
"installer-name": "promote-button"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
(function(){"use strict";function r(t,n,e,u,f,d,p,m){var o=typeof t=="function"?t.options:t;return n&&(o.render=n,o.staticRenderFns=e,o._compiled=!0),{exports:t,options:o}}const i={name:"KProfilesButton",props:{text:{String,default:"Profiles"},icon:{String,default:"account"},theme:{String,default:"pink-icon"},title:{String,default:""},items:{Array,default:()=>[]}},computed:{options(){return this.items.map(t=>({text:t.text,icon:t.icon,disabled:!!t.disabled,click:()=>this.go(t),target:t.target||null}))},anchor(){const t=this.$refs.btn;return t&&(t.$el||t)}},mounted(){console.log("[ProfilesButton] mounted",this.options)},methods:{toggle(){const t=this.$refs.menu;t&&this.anchor&&t.toggle()},onClose(){},go(t){t.disabled||t!=null&&t.link&&window.open(t.link,t.target||"_blank")}}};var s=function(){var n=this,e=n._self._c;return e("div",{staticClass:"k-profiles-button"},[e("k-button",{ref:"btn",attrs:{dropdown:!0,title:n.title,variant:"filled",icon:n.icon,size:"sm",text:n.text,theme:n.theme},on:{click:function(u){return n.$refs.menu.toggle()}}}),e("k-dropdown-content",{ref:"menu",attrs:{alignX:"end",anchor:n.anchor,options:n.options},on:{close:n.onClose}})],1)},l=[],c=r(i,s,l);const a=c.exports;panel.plugin("scottboms/promote-button",{components:{"k-profiles-button":a}})})();
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
<?php
|
||||
|
||||
load(['scottboms\Promote\PlatformPromoter' => __DIR__ . '/classes/PlatformPromoter.php']);
|
||||
|
||||
use ScottBoms\Promote\PlatformPromoter;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Cms\App;
|
||||
|
||||
// shamelessly borrowed from distantnative/retour-for-kirby
|
||||
if (
|
||||
version_compare(App::version() ?? '0.0.0', '5.0.0', '<') === true ||
|
||||
version_compare(App::version() ?? '0.0.0', '6.0.0', '>=') === true
|
||||
) {
|
||||
throw new Exception('Promote Button requires Kirby v4 or v5');
|
||||
}
|
||||
|
||||
Kirby::plugin('scottboms/promote-button', [
|
||||
|
||||
'areas' => [
|
||||
'site' => function () {
|
||||
return [
|
||||
'buttons' => [
|
||||
'promote' => function ($page) {
|
||||
return [
|
||||
'icon' => 'megaphone',
|
||||
'text' => 'Promouvoir',
|
||||
'theme' => 'pink-icon',
|
||||
'title' => 'Share this Page',
|
||||
'dialog' => 'promote/?page=' . $page->uuid()->toString(),
|
||||
];
|
||||
},
|
||||
|
||||
'profiles' => function () {
|
||||
$services = option('scottboms.promote.services', []);
|
||||
$items = [];
|
||||
|
||||
foreach ($services as $service) {
|
||||
if ($service === 'mastodon') {
|
||||
$host = trim((string) option('scottboms.promote.mastodon.url'));
|
||||
$user = trim((string) option('scottboms.promote.mastodon.username'));
|
||||
if ($host && $user) {
|
||||
$items[] = [
|
||||
'text' => 'Mastodon',
|
||||
'icon' => 'mastodon', // keep generic to rule out custom icon issues
|
||||
'link' => 'https://' . $host . '/@' . $user,
|
||||
'target' => '_blank',
|
||||
];
|
||||
}
|
||||
} elseif ($service === 'bluesky') {
|
||||
$handle = trim((string) option('scottboms.promote.bluesky.handle'));
|
||||
if ($handle) {
|
||||
$items[] = [
|
||||
'text' => 'Bluesky',
|
||||
'icon' => 'bluesky',
|
||||
'link' => 'https://bsky.app/profile/' . $handle,
|
||||
'target' => '_blank',
|
||||
];
|
||||
}
|
||||
} elseif ($service === 'linkedin') {
|
||||
$username = trim((string) option('scottboms.promote.linkedin.username'));
|
||||
if ($username) {
|
||||
$items[] = [
|
||||
'text' => 'LinkedIn',
|
||||
'icon' => 'linkedin',
|
||||
'link' => 'https://linkedin.com/in/' . $username,
|
||||
'target' => '_blank',
|
||||
];
|
||||
}
|
||||
} elseif ($service === 'instagram') {
|
||||
$username = trim((string) option('scottboms.promote.instagram.username'));
|
||||
if ($username) {
|
||||
$items[] = [
|
||||
'text' => 'Instagram',
|
||||
'icon' => 'instagram',
|
||||
'link' => 'https://instagram.com/' . $username,
|
||||
'target' => '_blank',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$items) {
|
||||
$items[] = [
|
||||
'text' => 'No profiles configured',
|
||||
'icon' => 'alert',
|
||||
'disabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'component' => 'k-profiles-button',
|
||||
'props' => [
|
||||
'text' => 'Profiles',
|
||||
'icon' => 'account',
|
||||
'theme' => 'pink-icon',
|
||||
'title' => 'Profiles',
|
||||
'items' => $items,
|
||||
],
|
||||
];
|
||||
},
|
||||
|
||||
],
|
||||
|
||||
'dialogs' => [
|
||||
'promote' => [
|
||||
'load' => function () {
|
||||
$page = page(get('page'));
|
||||
return [
|
||||
'component' => 'k-form-dialog',
|
||||
'props' => [
|
||||
'size' => 'large',
|
||||
'fields' => [
|
||||
'text' => [
|
||||
'label' => 'Texte',
|
||||
'type' => 'textarea',
|
||||
'buttons' => false,
|
||||
'size' => 'small',
|
||||
'required' => true,
|
||||
],
|
||||
'platforms' => [
|
||||
'label' => 'Publier sur',
|
||||
'type' => 'checkboxes',
|
||||
'columns' => max(1, min(3, count(option('scottboms.promote.services', [])))),
|
||||
'options' => array_map(function ($service) {
|
||||
$labelMap = [
|
||||
'mastodon' => 'Mastodon',
|
||||
'bluesky' => 'Bluesky',
|
||||
'linkedin' => 'LinkedIn',
|
||||
'instagram' => 'Instagram',
|
||||
];
|
||||
return [
|
||||
'value' => $service,
|
||||
'text' => $labelMap[$service] ?? ucfirst($service)
|
||||
];
|
||||
}, option('scottboms.promote.services', []))
|
||||
],
|
||||
],
|
||||
'value' => [
|
||||
'text' => 'Just posted ' . $page->title()->value() . ' ' . (
|
||||
option('scottboms.promote.host_url')
|
||||
? rtrim(option('scottboms.promote.host_url'), '/') . '/' . $page->uri()
|
||||
: $page->url()
|
||||
),
|
||||
'platforms' => ['mastodon','bluesky','linkedin'],
|
||||
],
|
||||
'submitButton' => [
|
||||
'icon' => 'megaphone',
|
||||
'text' => 'Poster',
|
||||
'theme' => 'pink'
|
||||
],
|
||||
],
|
||||
];
|
||||
},
|
||||
'submit' => function () {
|
||||
$text = get('text');
|
||||
$enabled = option('scottboms.promote.services', []);
|
||||
$platforms = array_intersect(get('platforms', []), $enabled);
|
||||
if (!is_array($platforms)) return false;
|
||||
|
||||
$promoter = new ScottBoms\Promote\PlatformPromoter();
|
||||
foreach ($platforms as $platform) {
|
||||
$promoter->post($platform, $text);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
],
|
||||
],
|
||||
];
|
||||
},
|
||||
],
|
||||
|
||||
'info' => [
|
||||
'homepage' => 'https://github.com/scottboms/kirby-promote-button',
|
||||
'version' => '1.1.1',
|
||||
'license' => 'MIT',
|
||||
'authors' => [[ 'name' => 'Scott Boms' ]],
|
||||
],
|
||||
|
||||
]);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"name": "promote-button",
|
||||
"description": "Promote Button",
|
||||
"author": "Scott Boms <plugins@scottboms.com>",
|
||||
"version": "1.1.1",
|
||||
"type": "kirby-plugin",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "kirbyup serve src/index.js",
|
||||
"build": "kirbyup src/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"kirbyup": "^3.1.5"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 92 KiB |
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<div class="k-profiles-button">
|
||||
<k-button
|
||||
:dropdown="true"
|
||||
:title="title"
|
||||
variant="filled"
|
||||
:icon="icon"
|
||||
ref="btn"
|
||||
size="sm"
|
||||
:text="text"
|
||||
:theme="theme"
|
||||
@click="$refs.menu.toggle()"
|
||||
/>
|
||||
|
||||
<k-dropdown-content
|
||||
ref="menu"
|
||||
alignX="end"
|
||||
:anchor="anchor"
|
||||
:options="options"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "KProfilesButton",
|
||||
props: {
|
||||
text: { String, default: 'Profiles' },
|
||||
icon: { String, default: 'account' },
|
||||
theme: { String, default: 'pink-icon' },
|
||||
title: { String, default: '' },
|
||||
items: { Array, default: () => [] }
|
||||
},
|
||||
|
||||
computed: {
|
||||
options() {
|
||||
// k-dropdown-content expects standard dropdown option shape:
|
||||
// { text, icon, link, target, disabled, click, ... }
|
||||
return this.items.map((it) => ({
|
||||
text: it.text,
|
||||
icon: it.icon,
|
||||
disabled: !!it.disabled,
|
||||
click: () => this.go(it),
|
||||
target: it.target || null,
|
||||
}));
|
||||
},
|
||||
|
||||
anchor() {
|
||||
// k-dropdown expects a real dom node
|
||||
const r = this.$refs.btn;
|
||||
return r && (r.$el || r);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log('[ProfilesButton] mounted', this.options);
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
const menu = this.$refs.menu;
|
||||
if (!menu) return;
|
||||
|
||||
// ensure we have an anchor before toggling
|
||||
if (!this.anchor) return;
|
||||
menu.toggle();
|
||||
},
|
||||
|
||||
onClose() {
|
||||
// console.log('[ProfilesButton] dropdown closed');
|
||||
},
|
||||
|
||||
go(item) {
|
||||
// console.log('[ProfilesButton] go →', item);
|
||||
if (item.disabled) return;
|
||||
if (item?.link) window.open(item.link, item.target || "_blank");
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import ProfilesButton from "./components/ProfilesButton.vue";
|
||||
|
||||
panel.plugin("scottboms/promote-button", {
|
||||
components: {
|
||||
"k-profiles-button": ProfilesButton,
|
||||
}
|
||||
});
|
||||