Sync folders

This commit is contained in:
Andreas Andersen 2026-04-18 23:19:08 +02:00
parent 20c4ea5875
commit 19026f9fa0
28 changed files with 412 additions and 101 deletions

View file

@ -1,3 +1,38 @@
# Ansico-plugins # Ansico Plugins
WP plugin that makes it possible to install and update other Ansico plugins on your WP site. It connect to this Forgejo server. Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update and delete internal plugins from Ansico releases.
## Installation
1. Upload the plugin ZIP through WordPress, or copy the plugin folder to `/wp-content/plugins/`.
2. Activate **Ansico Plugins**.
3. Open **Settings → Ansico Plugins**.
4. Review the default Ansico settings or click **Reset til Ansico standard**.
5. Open **Plugins → Ansico Plugins** to browse available plugins.
## Changelog
### 1.0.0.2
- Added support for parsing `readme.txt` in WordPress format.
- Added tabbed plugin details view with Description, Installation, Changelog and Screenshots.
- Added generated plugin icon and plugin banner assets.
- Improved metadata extraction priority so `readme.txt` can drive plugin presentation.
### 1.0.0
- First stable release.
- WordPress-style plugin catalogue UI.
- Details popup with metadata and optional screenshot.
- Install, activate, reinstall, update and delete actions.
- Self-update support for Ansico Plugins.
- Ansico default configuration and reset button.
## Screenshots
1. The Ansico Plugins catalogue in WordPress admin.
2. The plugin details modal with tabs and metadata.
## Support
Support URL: https://ansico.dk/Ansico/Ansico-plugins

BIN
README.md:Zone.Identifier Normal file

Binary file not shown.

BIN
ansico-plugins-1.0.0.2.zip Normal file

Binary file not shown.

Binary file not shown.

View file

@ -1,45 +1,38 @@
# Ansico Plugins # Ansico Plugins
Version: 1.0.0 Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update and delete internal plugins from Ansico releases.
Contributor: aphandersen
Forfatter: Andreas Andersen (Ansico) ## Installation
Author URL: https://ansico.dk
1. Upload the plugin ZIP through WordPress, or copy the plugin folder to `/wp-content/plugins/`.
2. Activate **Ansico Plugins**.
3. Open **Settings → Ansico Plugins**.
4. Review the default Ansico settings or click **Reset til Ansico standard**.
5. Open **Plugins → Ansico Plugins** to browse available plugins.
## Changelog
### 1.0.0.2
- Added support for parsing `readme.txt` in WordPress format.
- Added tabbed plugin details view with Description, Installation, Changelog and Screenshots.
- Added generated plugin icon and plugin banner assets.
- Improved metadata extraction priority so `readme.txt` can drive plugin presentation.
### 1.0.0
- First stable release.
- WordPress-style plugin catalogue UI.
- Details popup with metadata and optional screenshot.
- Install, activate, reinstall, update and delete actions.
- Self-update support for Ansico Plugins.
- Ansico default configuration and reset button.
## Screenshots
1. The Ansico Plugins catalogue in WordPress admin.
2. The plugin details modal with tabs and metadata.
## Support
Support URL: https://ansico.dk/Ansico/Ansico-plugins Support URL: https://ansico.dk/Ansico/Ansico-plugins
Licens: GPL-3.0-or-later
Testet på WordPress: 6.9.4
Kræver PHP: 7.0
## Beskrivelse
Gives access to Ansico plugin directory with Ansico plugins.
## Formål
Dette plugin forbinder WordPress til Ansico's Forgejo-server og giver adgang til Ansico plugins gennem et privat plugin-katalog.
## Funktioner
- Viser interne plugin-repositories fra Ansico
- Åbner en WordPress-lignende popup med plugin-detaljer
- Kan installere, aktivere, geninstallere, opdatere og afinstallere plugins
- Kan læse metadata fra repository og plugin-header
- Kan vise `screenshot.png`, hvis filen findes i repository-roden
- Kan opdatere Ansico Plugins selv via release-systemet
- Leveres med Ansico-standardindstillinger og reset-knap
## Standardindstillinger
Ved første installation er disse værdier udfyldt:
- Forgejo URL: `https://ansico.dk`
- Bruger eller organisation: `Ansico`
- Access token: tom
- Topic-filter: `wordpress-plugin`
- Owner type: `Organisation`
- Selvopdatering: aktiv
- Ansico plugins owner: `Ansico`
- Ansico plugins repository: `Ansico-plugins`
## Release
Denne version er klargjort som officiel release `1.0.0`.

View file

@ -3,7 +3,7 @@
* Plugin Name: Ansico Plugins * Plugin Name: Ansico Plugins
* Plugin URI: https://ansico.dk * Plugin URI: https://ansico.dk
* Description: Gives access to Ansico plugin directory with Ansico plugins * Description: Gives access to Ansico plugin directory with Ansico plugins
* Version: 1.0.0 * Version: 1.0.0.2
* Author: Andreas Andersen (Ansico) * Author: Andreas Andersen (Ansico)
* Author URI: https://ansico.dk * Author URI: https://ansico.dk
* Contributors: aphandersen * Contributors: aphandersen
@ -20,7 +20,7 @@ if (!defined('ABSPATH')) {
exit; exit;
} }
define('ANSICO_PLUGINS_VERSION', '1.0.0'); define('ANSICO_PLUGINS_VERSION', '1.0.0.2');
define('ANSICO_PLUGINS_MANAGED_OPTION', 'ansico_plugins_managed_plugins'); define('ANSICO_PLUGINS_MANAGED_OPTION', 'ansico_plugins_managed_plugins');
define('ANSICO_PLUGINS_FILE', __FILE__); define('ANSICO_PLUGINS_FILE', __FILE__);
define('ANSICO_PLUGINS_DIR', plugin_dir_path(__FILE__)); define('ANSICO_PLUGINS_DIR', plugin_dir_path(__FILE__));

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

View file

@ -333,13 +333,13 @@ class Ansico_Plugins_Admin {
echo '<div class="ansico-plugin-card">'; echo '<div class="ansico-plugin-card">';
echo '<div class="ansico-plugin-card__top">'; echo '<div class="ansico-plugin-card__top">';
echo '<div class="ansico-plugin-icon">AP</div>'; echo $this->get_plugin_icon_markup($metadata, $display_name);
echo '<div class="ansico-plugin-meta">'; echo '<div class="ansico-plugin-meta">';
echo '<h2><a href="' . esc_url($details_url) . '" class="thickbox">' . esc_html($display_name) . '</a></h2>'; echo '<h2><a href="' . esc_url($details_url) . '" class="thickbox">' . esc_html($display_name) . '</a></h2>';
echo '<div class="ansico-plugin-submeta">' . $badge . '</div>'; echo '<div class="ansico-plugin-submeta">' . $badge . '</div>';
echo '</div></div>'; echo '</div></div>';
echo '<p class="ansico-plugin-description">' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '</p>'; echo '<p class="ansico-plugin-description">' . esc_html(!empty($metadata['description']) ? $metadata['description'] : (!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins'))) . '</p>';
echo '<div class="ansico-plugin-facts">'; echo '<div class="ansico-plugin-facts">';
echo '<span><strong>' . esc_html__('Installeret', 'ansico-plugins') . ':</strong> ' . esc_html($plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—') . '</span>'; echo '<span><strong>' . esc_html__('Installeret', 'ansico-plugins') . ':</strong> ' . esc_html($plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—') . '</span>';
@ -563,12 +563,20 @@ class Ansico_Plugins_Admin {
private function get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata) { private function get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata) {
$display_name = !empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name']; $display_name = !empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name'];
$description = !empty($metadata['description']) ? $metadata['description'] : (!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins'));
$sections = !empty($metadata['readme_sections']) && is_array($metadata['readme_sections']) ? $metadata['readme_sections'] : array();
$is_wordpress_readme = !empty($metadata['readme_source']) && preg_match('/\.txt$/i', $metadata['readme_source']);
$html = ''; $html = '';
if (!empty($metadata['banner_url'])) {
$html .= '<div class="ansico-plugin-banner"><img src="' . esc_url($metadata['banner_url']) . '" alt="' . esc_attr($display_name) . '" /></div>';
}
$html .= '<div class="ansico-plugin-hero">'; $html .= '<div class="ansico-plugin-hero">';
$html .= '<div class="ansico-plugin-icon">AP</div>'; $html .= $this->get_plugin_icon_markup($metadata, $display_name);
$html .= '<div>'; $html .= '<div>';
$html .= '<h1 style="margin:0 0 8px;font-size:26px;line-height:1.2">' . esc_html($display_name) . '</h1>'; $html .= '<h1 style="margin:0 0 8px;font-size:26px;line-height:1.2">' . esc_html($display_name) . '</h1>';
$html .= '<p class="description" style="margin:0 0 10px">' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '</p>'; $html .= '<p class="description" style="margin:0 0 10px">' . esc_html($description) . '</p>';
$html .= $this->get_state_badge($plugin_state); $html .= $this->get_state_badge($plugin_state);
$html .= '</div></div>'; $html .= '</div></div>';
@ -579,7 +587,6 @@ class Ansico_Plugins_Admin {
$html .= '<a class="button" href="' . esc_url($repo['html_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Åbn repository', 'ansico-plugins') . '</a>'; $html .= '<a class="button" href="' . esc_url($repo['html_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Åbn repository', 'ansico-plugins') . '</a>';
$html .= '</div>'; $html .= '</div>';
$html .= '<div class="ansico-meta-grid">';
$meta_rows = array( $meta_rows = array(
__('Installeret version', 'ansico-plugins') => $plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—', __('Installeret version', 'ansico-plugins') => $plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—',
__('Seneste release', 'ansico-plugins') => !is_wp_error($release) ? $this->normalize_release_version($release) : '—', __('Seneste release', 'ansico-plugins') => !is_wp_error($release) ? $this->normalize_release_version($release) : '—',
@ -592,8 +599,71 @@ class Ansico_Plugins_Admin {
__('Plugin-URL', 'ansico-plugins') => !empty($metadata['plugin_headers']['PluginURI']) ? $metadata['plugin_headers']['PluginURI'] : '—', __('Plugin-URL', 'ansico-plugins') => !empty($metadata['plugin_headers']['PluginURI']) ? $metadata['plugin_headers']['PluginURI'] : '—',
__('Kræver WordPress', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresWP']) ? $metadata['plugin_headers']['RequiresWP'] : '—', __('Kræver WordPress', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresWP']) ? $metadata['plugin_headers']['RequiresWP'] : '—',
__('Kræver PHP', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresPHP']) ? $metadata['plugin_headers']['RequiresPHP'] : '—', __('Kræver PHP', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresPHP']) ? $metadata['plugin_headers']['RequiresPHP'] : '—',
__('Text domain', 'ansico-plugins') => !empty($metadata['plugin_headers']['TextDomain']) ? $metadata['plugin_headers']['TextDomain'] : '—',
); );
if (!empty($metadata['plugin_headers']['TextDomain'])) {
$meta_rows[__('Text domain', 'ansico-plugins')] = $metadata['plugin_headers']['TextDomain'];
}
$tabs = array();
$tabs[] = array(
'slug' => 'description',
'label' => __('Beskrivelse', 'ansico-plugins'),
'content' => $this->render_readme_section(!empty($sections['description']) ? $sections['description'] : $description, $is_wordpress_readme),
);
if (!empty($sections['installation'])) {
$tabs[] = array(
'slug' => 'installation',
'label' => __('Installation', 'ansico-plugins'),
'content' => $this->render_readme_section($sections['installation'], $is_wordpress_readme),
);
}
if (!empty($sections['changelog'])) {
$tabs[] = array(
'slug' => 'changelog',
'label' => __('Changelog', 'ansico-plugins'),
'content' => $this->render_readme_section($sections['changelog'], $is_wordpress_readme),
);
}
if (!empty($metadata['screenshot_url']) || !empty($sections['screenshots'])) {
$screen = '';
if (!empty($metadata['screenshot_url'])) {
$screen .= '<p><img src="' . esc_url($metadata['screenshot_url']) . '" alt="' . esc_attr(sprintf(__('Screenshot for %s', 'ansico-plugins'), $display_name)) . '" /></p>';
}
if (!empty($sections['screenshots'])) {
$screen .= $this->render_readme_section($sections['screenshots'], $is_wordpress_readme);
}
$tabs[] = array(
'slug' => 'screenshots',
'label' => __('Screenshots', 'ansico-plugins'),
'content' => $screen,
);
}
$tabs[] = array(
'slug' => 'metadata',
'label' => __('Metadata', 'ansico-plugins'),
'content' => $this->render_metadata_grid($meta_rows),
);
if (!is_wp_error($release) && !empty($release['body'])) {
$tabs[] = array(
'slug' => 'release-notes',
'label' => __('Release-noter', 'ansico-plugins'),
'content' => '<div class="ansico-release-notes">' . wp_kses_post(wpautop($release['body'])) . '</div>',
);
}
$html .= $this->render_details_tabs($tabs, $repo['name']);
return $html;
}
private function get_plugin_icon_markup($metadata, $display_name) {
if (!empty($metadata['icon_url'])) {
return '<div class="ansico-plugin-icon ansico-plugin-icon--image"><img src="' . esc_url($metadata['icon_url']) . '" alt="' . esc_attr($display_name) . '" /></div>';
}
return '<div class="ansico-plugin-icon">AP</div>';
}
private function render_metadata_grid($meta_rows) {
$html = '<div class="ansico-meta-grid">';
foreach ($meta_rows as $label => $value) { foreach ($meta_rows as $label => $value) {
$html .= '<p><strong>' . esc_html($label) . '</strong><br>'; $html .= '<p><strong>' . esc_html($label) . '</strong><br>';
if (is_string($value) && preg_match('#^https?://#i', $value)) { if (is_string($value) && preg_match('#^https?://#i', $value)) {
@ -604,22 +674,59 @@ class Ansico_Plugins_Admin {
$html .= '</p>'; $html .= '</p>';
} }
$html .= '</div>'; $html .= '</div>';
if (!empty($metadata['screenshot_url'])) {
$html .= '<div class="ansico-screenshot" style="margin:22px 0">';
$html .= '<h2>' . esc_html__('Screenshot', 'ansico-plugins') . '</h2>';
$html .= '<img src="' . esc_url($metadata['screenshot_url']) . '" alt="' . esc_attr(sprintf(__('Screenshot for %s', 'ansico-plugins'), $display_name)) . '" />';
$html .= '</div>';
}
if (!is_wp_error($release) && !empty($release['body'])) {
$html .= '<h2>' . esc_html__('Release-noter', 'ansico-plugins') . '</h2>';
$html .= '<div class="ansico-release-notes">' . wp_kses_post(wpautop($release['body'])) . '</div>';
}
return $html; return $html;
} }
private function render_details_tabs($tabs, $repo_name) {
$id_base = 'ansico-tabs-' . sanitize_title($repo_name);
$html = '<div class="ansico-tabs"><div class="ansico-tab-nav" role="tablist">';
foreach ($tabs as $index => $tab) {
$active = 0 === $index ? ' is-active' : '';
$html .= '<button type="button" class="ansico-tab-button' . $active . '" data-target="#' . esc_attr($id_base . '-' . $tab['slug']) . '">' . esc_html($tab['label']) . '</button>';
}
$html .= '</div>';
foreach ($tabs as $index => $tab) {
$active = 0 === $index ? ' is-active' : '';
$html .= '<div id="' . esc_attr($id_base . '-' . $tab['slug']) . '" class="ansico-tab-panel' . $active . '">' . $tab['content'] . '</div>';
}
$html .= '</div>';
return $html;
}
private function render_readme_section($content, $is_wordpress_readme = false) {
$content = trim((string) $content);
if ('' === $content) {
return '<p>' . esc_html__('Ingen oplysninger tilgængelige.', 'ansico-plugins') . '</p>';
}
if ($is_wordpress_readme) {
return wp_kses_post(wpautop($this->format_wordpress_readme_text($content)));
}
return wp_kses_post(wpautop($this->format_markdown_text($content)));
}
private function format_wordpress_readme_text($content) {
$content = preg_replace('/^=\s*(.+?)\s*=\s*$/m', "
<strong>$1</strong>
", $content);
$content = preg_replace('/^\*\s+/m', '• ', $content);
return $content;
}
private function format_markdown_text($content) {
$content = preg_replace('/^###\s+(.+)$/m', "
<strong>$1</strong>
", $content);
$content = preg_replace('/^##\s+(.+)$/m', "
<strong>$1</strong>
", $content);
$content = preg_replace('/^#\s+(.+)$/m', "
<strong>$1</strong>
", $content);
$content = preg_replace('/^[-*]\s+/m', '• ', $content);
$content = preg_replace('/`([^`]+)`/', '$1', $content);
return $content;
}
private function render_catalog_styles() { private function render_catalog_styles() {
echo '<style> echo '<style>
.ansico-directory-header{display:flex;justify-content:space-between;align-items:center;margin:16px 0 20px} .ansico-directory-header{display:flex;justify-content:space-between;align-items:center;margin:16px 0 20px}
@ -631,7 +738,7 @@ class Ansico_Plugins_Admin {
.ansico-plugin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px;margin-top:18px} .ansico-plugin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px;margin-top:18px}
.ansico-plugin-card{background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04);display:flex;flex-direction:column;gap:14px} .ansico-plugin-card{background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04);display:flex;flex-direction:column;gap:14px}
.ansico-plugin-card__top{display:flex;gap:14px;align-items:flex-start} .ansico-plugin-card__top{display:flex;gap:14px;align-items:flex-start}
.ansico-plugin-icon{width:64px;height:64px;border-radius:12px;background:#2271b1;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;flex-shrink:0} .ansico-plugin-icon{width:64px;height:64px;border-radius:12px;background:#2271b1;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;flex-shrink:0;overflow:hidden}.ansico-plugin-icon--image{background:#fff;border:1px solid #dcdcde}.ansico-plugin-icon img{width:100%;height:100%;object-fit:cover;display:block}
.ansico-plugin-meta h2{margin:0 0 6px;font-size:18px;line-height:1.3} .ansico-plugin-meta h2{margin:0 0 6px;font-size:18px;line-height:1.3}
.ansico-plugin-meta h2 a{text-decoration:none} .ansico-plugin-meta h2 a{text-decoration:none}
.ansico-plugin-submeta{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:#646970} .ansico-plugin-submeta{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:#646970}
@ -647,12 +754,12 @@ class Ansico_Plugins_Admin {
.ansico-plugin-hero .ansico-plugin-icon{width:72px;height:72px;font-size:22px} .ansico-plugin-hero .ansico-plugin-icon{width:72px;height:72px;font-size:22px}
.ansico-meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 20px;margin:18px 0} .ansico-meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 20px;margin:18px 0}
.ansico-meta-grid p{margin:0} .ansico-meta-grid p{margin:0}
.ansico-screenshot img{max-width:100%;height:auto;border:1px solid #dcdcde;border-radius:8px} .ansico-plugin-banner{margin:0 0 18px}.ansico-plugin-banner img{width:100%;height:auto;border-radius:10px;border:1px solid #dcdcde;display:block}.ansico-screenshot img,.ansico-tab-panel img{max-width:100%;height:auto;border:1px solid #dcdcde;border-radius:8px}
.ansico-modal-actions{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0} .ansico-modal-actions{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
.ansico-release-notes{background:#f6f7f7;border-radius:8px;padding:14px} .ansico-release-notes{background:#f6f7f7;border-radius:8px;padding:14px}
.ansico-thickbox-content{padding:20px 24px 24px} .ansico-thickbox-content{padding:20px 24px 24px}.ansico-tabs{margin-top:18px}.ansico-tab-nav{display:flex;gap:8px;flex-wrap:wrap;border-bottom:1px solid #dcdcde;margin-bottom:16px;padding-bottom:8px}.ansico-tab-button{background:#f6f7f7;border:1px solid #dcdcde;border-radius:6px 6px 0 0;padding:8px 12px;cursor:pointer}.ansico-tab-button.is-active{background:#fff;border-bottom-color:#fff;font-weight:600}.ansico-tab-panel{display:none}.ansico-tab-panel.is-active{display:block}
#TB_window{max-height:81vh !important}#TB_ajaxContent{max-height:calc(81vh - 45px) !important;height:auto !important;overflow:auto !important}#TB_ajaxContent .ansico-thickbox-content{width:auto !important;height:auto !important;max-height:calc(81vh - 65px) !important;overflow:auto !important;padding:20px 24px 24px !important;box-sizing:border-box} #TB_window{max-height:81vh !important}#TB_ajaxContent{max-height:calc(81vh - 45px) !important;height:auto !important;overflow:auto !important}#TB_ajaxContent .ansico-thickbox-content{width:auto !important;height:auto !important;max-height:calc(81vh - 65px) !important;overflow:auto !important;padding:20px 24px 24px !important;box-sizing:border-box}
@media (max-width: 960px){.ansico-meta-grid{grid-template-columns:1fr}} @media (max-width: 960px){.ansico-meta-grid{grid-template-columns:1fr}}
</style>'; </style><script>document.addEventListener("click",function(e){var btn=e.target.closest(".ansico-tab-button");if(!btn){return;}var wrap=btn.closest(".ansico-tabs");if(!wrap){return;}var targetSel=btn.getAttribute("data-target");wrap.querySelectorAll(".ansico-tab-button").forEach(function(el){el.classList.remove("is-active");});wrap.querySelectorAll(".ansico-tab-panel").forEach(function(el){el.classList.remove("is-active");});btn.classList.add("is-active");var panel=wrap.querySelector(targetSel);if(panel){panel.classList.add("is-active");}});</script>';
} }
} }

View file

@ -303,23 +303,188 @@ class Ansico_Plugins_Client {
public function get_repository_readme_title($owner, $repo_name, $ref = '') { public function get_repository_readme_title($owner, $repo_name, $ref = '') {
$candidates = array('README.md', 'readme.md', 'Readme.md'); $documentation = $this->get_repository_documentation($owner, $repo_name, $ref);
if (is_wp_error($documentation)) {
return '';
}
return !empty($documentation['title']) ? (string) $documentation['title'] : '';
}
public function get_repository_documentation($owner, $repo_name, $ref = '') {
$candidates = array('readme.txt', 'README.txt', 'README.md', 'readme.md', 'Readme.md');
foreach ($candidates as $candidate) { foreach ($candidates as $candidate) {
$file = $this->get_repository_contents($owner, $repo_name, $candidate, $ref); $raw = $this->get_repository_text_file($owner, $repo_name, $candidate, $ref);
if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) { if ('' === $raw) {
continue; continue;
} }
$raw = base64_decode((string) $file['content'], true); if (preg_match('/\.txt$/i', $candidate)) {
if (false === $raw) { $parsed = $this->parse_wordpress_readme($raw);
continue; } else {
$parsed = $this->parse_markdown_readme($raw);
} }
if (preg_match('/^\#\s+(.+)$/m', (string) $raw, $matches)) { if (!empty($parsed['title']) || !empty($parsed['short_description']) || !empty($parsed['sections'])) {
return trim(wp_strip_all_tags($matches[1])); $parsed['source'] = $candidate;
return $parsed;
} }
} }
return array(
'title' => '',
'short_description' => '',
'sections' => array(),
'source' => '',
);
}
private function get_repository_text_file($owner, $repo_name, $path, $ref = '') {
$file = $this->get_repository_contents($owner, $repo_name, $path, $ref);
if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) {
return '';
}
$raw = base64_decode((string) $file['content'], true);
if (false === $raw) {
return '';
}
return str_replace(array("
", " "), "
", (string) $raw);
}
private function parse_wordpress_readme($raw) {
$raw = trim((string) $raw);
$lines = preg_split('/
/', $raw);
$title = '';
$short_description = '';
$sections = array();
$current = '';
$buffer = array();
$header_done = false;
foreach ($lines as $line) {
if ('' === $title && preg_match('/^===\s*(.+?)\s*===\s*$/', $line, $m)) {
$title = trim($m[1]);
continue;
}
if (!$header_done && preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) {
$header_done = true;
$current = strtolower(trim($m[1]));
$buffer = array();
continue;
}
if (!$header_done) {
if ('' === $short_description && '' !== trim($line) && false === strpos($line, ':')) {
$short_description = trim($line);
}
continue;
}
if (preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) {
if ('' !== $current) {
$sections[$current] = trim(implode("
", $buffer));
}
$current = strtolower(trim($m[1]));
$buffer = array();
continue;
}
$buffer[] = $line;
}
if ('' !== $current) {
$sections[$current] = trim(implode("
", $buffer));
}
if ('' === $short_description && !empty($sections['description'])) {
$short_description = $this->first_paragraph($sections['description']);
}
return array(
'title' => $title,
'short_description' => $short_description,
'sections' => $sections,
);
}
private function parse_markdown_readme($raw) {
$title = '';
$short_description = '';
$sections = array();
$current = '';
$buffer = array();
$after_title = '';
foreach (preg_split('/
/', (string) $raw) as $line) {
if ('' === $title && preg_match('/^#\s+(.+)$/', $line, $m)) {
$title = trim(wp_strip_all_tags($m[1]));
continue;
}
if (preg_match('/^##\s+(.+)$/', $line, $m)) {
if ('' !== $current) {
$sections[$current] = trim(implode("
", $buffer));
}
$current = strtolower(trim(wp_strip_all_tags($m[1])));
$buffer = array();
continue;
}
if ('' === $current) {
$after_title .= $line . "
";
} else {
$buffer[] = $line;
}
}
if ('' !== $current) {
$sections[$current] = trim(implode("
", $buffer));
}
$short_description = $this->first_paragraph($after_title);
if ('' === $short_description && !empty($sections['description'])) {
$short_description = $this->first_paragraph($sections['description']);
}
return array(
'title' => $title,
'short_description' => $short_description,
'sections' => $sections,
);
}
private function first_paragraph($text) {
$text = trim((string) $text);
if ('' === $text) {
return '';
}
$parts = preg_split("/
\s*
/", $text);
if (empty($parts[0])) {
return '';
}
return trim(wp_strip_all_tags($parts[0]));
}
private function find_repository_asset_url($owner, $repo_name, $default_branch, $candidates) {
foreach ((array) $candidates as $candidate) {
$file = $this->get_repository_contents($owner, $repo_name, $candidate, $default_branch);
if (!is_wp_error($file) && is_array($file) && !empty($file['download_url'])) {
return (string) $file['download_url'];
}
}
return ''; return '';
} }
@ -331,54 +496,53 @@ class Ansico_Plugins_Client {
$default_branch = !empty($repo['default_branch']) ? (string) $repo['default_branch'] : ''; $default_branch = !empty($repo['default_branch']) ? (string) $repo['default_branch'] : '';
$root = $this->get_repository_contents($owner, $repo_name, '', $default_branch); $root = $this->get_repository_contents($owner, $repo_name, '', $default_branch);
if (is_wp_error($root) || !is_array($root)) {
return array(
'readme_title' => '',
'author_name' => !empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''),
'author_url' => !empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : ''),
'plugin_headers' => array(),
'support_url' => !empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : '',
'license' => !empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : '',
'screenshot_url' => '',
'default_branch' => $default_branch,
);
}
$screenshot_url = '';
$plugin_headers = array(); $plugin_headers = array();
foreach ($root as $item) {
if (!is_array($item) || empty($item['name'])) { if (!is_wp_error($root) && is_array($root)) {
continue; foreach ($root as $item) {
} if (!is_array($item) || empty($item['name'])) {
$name = (string) $item['name']; continue;
if (strtolower($name) === 'screenshot.png' && !empty($item['download_url'])) { }
$screenshot_url = (string) $item['download_url']; $name = (string) $item['name'];
} if (empty($plugin_headers) && !empty($item['type']) && 'file' === $item['type'] && preg_match('/\.php$/i', $name)) {
if (empty($plugin_headers) && !empty($item['type']) && 'file' === $item['type'] && preg_match('/\.php$/i', $name)) { $file = $this->get_repository_contents($owner, $repo_name, $name, $default_branch);
$file = $this->get_repository_contents($owner, $repo_name, $name, $default_branch); if (!is_wp_error($file) && is_array($file) && !empty($file['content']) && !empty($file['encoding']) && 'base64' === $file['encoding']) {
if (!is_wp_error($file) && is_array($file) && !empty($file['content']) && !empty($file['encoding']) && 'base64' === $file['encoding']) { $raw = base64_decode((string) $file['content'], true);
$raw = base64_decode((string) $file['content'], true); if (false !== $raw) {
if (false !== $raw) { $plugin_headers = $this->parse_plugin_headers($raw);
$plugin_headers = $this->parse_plugin_headers($raw); }
} }
} }
} }
} }
$readme_title = $this->get_repository_readme_title($owner, $repo_name, $default_branch); $documentation = $this->get_repository_documentation($owner, $repo_name, $default_branch);
if (is_wp_error($documentation)) {
$documentation = array('title' => '', 'short_description' => '', 'sections' => array(), 'source' => '');
}
$author_name = !empty($plugin_headers['Author']) ? (string) $plugin_headers['Author'] : (!empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : '')); $author_name = !empty($plugin_headers['Author']) ? (string) $plugin_headers['Author'] : (!empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''));
$author_url = !empty($plugin_headers['AuthorURI']) ? (string) $plugin_headers['AuthorURI'] : (!empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : '')); $author_url = !empty($plugin_headers['AuthorURI']) ? (string) $plugin_headers['AuthorURI'] : (!empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : ''));
$support_url = !empty($plugin_headers['SupportURI']) ? (string) $plugin_headers['SupportURI'] : (!empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : ''); $support_url = !empty($plugin_headers['SupportURI']) ? (string) $plugin_headers['SupportURI'] : (!empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : '');
$license = !empty($plugin_headers['License']) ? (string) $plugin_headers['License'] : (!empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : ''); $license = !empty($plugin_headers['License']) ? (string) $plugin_headers['License'] : (!empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : '');
$screenshot_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('screenshot.png', 'assets/screenshot.png'));
$icon_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('icon-256x256.png', 'icon-128x128.png', 'assets/icon-256x256.png', 'assets/icon-128x128.png'));
$banner_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('banner-1544x500.png', 'banner-772x250.png', 'assets/banner-1544x500.png', 'assets/banner-772x250.png'));
return array( return array(
'readme_title' => $readme_title, 'readme_title' => !empty($documentation['title']) ? $documentation['title'] : '',
'description' => !empty($documentation['short_description']) ? $documentation['short_description'] : (!empty($repo['description']) ? (string) $repo['description'] : ''),
'readme_sections' => !empty($documentation['sections']) && is_array($documentation['sections']) ? $documentation['sections'] : array(),
'readme_source' => !empty($documentation['source']) ? $documentation['source'] : '',
'author_name' => $author_name, 'author_name' => $author_name,
'author_url' => $author_url, 'author_url' => $author_url,
'plugin_headers' => $plugin_headers, 'plugin_headers' => $plugin_headers,
'support_url' => $support_url, 'support_url' => $support_url,
'license' => $license, 'license' => $license,
'screenshot_url' => $screenshot_url, 'screenshot_url' => $screenshot_url,
'icon_url' => $icon_url,
'banner_url' => $banner_url,
'default_branch' => $default_branch, 'default_branch' => $default_branch,
); );
} }

View file

@ -4,7 +4,7 @@ Tags: forgejo, plugins, private directory, ansico
Requires at least: 6.3 Requires at least: 6.3
Tested up to: 6.9.4 Tested up to: 6.9.4
Requires PHP: 7.0 Requires PHP: 7.0
Stable tag: 1.0.0 Stable tag: 1.0.0.2
License: GPL-3.0-or-later License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.html License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -12,7 +12,7 @@ Gives access to Ansico plugin directory with Ansico plugins.
== Description == == Description ==
Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update, and delete internal plugins from Ansico releases. Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update and delete internal plugins from Ansico releases.
= Features = = Features =
@ -21,9 +21,10 @@ Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin d
* Reads plugin metadata and README content from repositories. * Reads plugin metadata and README content from repositories.
* Opens a WordPress-style details popup for each plugin. * Opens a WordPress-style details popup for each plugin.
* Installs plugins from the latest Forgejo release ZIP. * Installs plugins from the latest Forgejo release ZIP.
* Supports activate, reinstall, update, and delete actions. * Supports activate, reinstall, update and delete actions.
* Supports self-updates for Ansico Plugins itself. * Supports self-updates for Ansico Plugins itself.
* Includes Ansico default settings and a reset button. * Includes Ansico default settings and a reset button.
* Supports plugin presentation from `readme.txt` in WordPress format.
== Installation == == Installation ==
@ -43,12 +44,23 @@ Yes. Public Ansico repositories can be listed without a token. A token can still
Yes. Self-update support is built in and can check the configured Ansico Plugins repository for newer releases. Yes. Self-update support is built in and can check the configured Ansico Plugins repository for newer releases.
== Screenshots ==
1. The Ansico Plugins catalogue in WordPress admin.
2. The plugin details modal with tabs and metadata.
== Changelog == == Changelog ==
= 1.0.0.2 =
* Added support for parsing `readme.txt` in WordPress format.
* Added tabbed plugin details view with Description, Installation, Changelog and Screenshots.
* Added generated plugin icon and plugin banner assets.
* Improved metadata extraction priority so `readme.txt` can drive plugin presentation.
= 1.0.0 = = 1.0.0 =
* First stable release. * First stable release.
* WordPress-style plugin catalogue UI. * WordPress-style plugin catalogue UI.
* Details popup with metadata and optional screenshot. * Details popup with metadata and optional screenshot.
* Install, activate, reinstall, update, and delete actions. * Install, activate, reinstall, update and delete actions.
* Self-update support for Ansico Plugins. * Self-update support for Ansico Plugins.
* Ansico default configuration and reset button. * Ansico default configuration and reset button.

BIN
banner-1544x500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

BIN
icon-256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.