1c93c82ef5
The v3.3.0 update checker only queried Gitea's /releases/latest endpoint, which requires a formal Release object (created via the Gitea web UI with optional notes + zip assets attached). A plain "git tag v3.3.x && git push --tags" from the terminal does NOT create that Release object — so the checker kept returning "No releases tagged on the Gitea repo yet" even when tags existed. wp_notes_fetch_latest_release() now falls back to the /tags?limit=1 endpoint when /releases/latest returns 404 (or any non-200). It synthesises a release-like payload from the newest tag — tag_name, html_url pointing at the tag view, tag message as the body, empty assets[] so the existing download-URL logic falls through to Gitea's source-archive URL pattern (/archive/<tag>.zip). Net effect: the "Check now" button now finds the latest version whether David creates formal Gitea Releases OR just pushes tags with "git push --tags". No workflow change required. Discovered while diagnosing why "Check now" wasn't seeing today's v3.1.0/v3.2.0/v3.3.0/v3.3.1 tags (just pushed in this session) — the tags were there, the formal Release objects were not. KNOWN LIMITATION (not a bug — flagged) The Gitea repo ranger/a-wp-notes-v3 is currently private. Anonymous API requests get a 404 (Gitea's standard behaviour for private repos). The updater code is correct but can't actually reach the API on a private repo without authentication. Fix: change the repo visibility to public on Gitea — appropriate anyway for a GPL-licensed plugin headed for the WordPress.org marketplace. VERSION BUMP - wp-notes.php header 3.3.1 → 3.3.2 - WP_NOTES_VERSION constant 3.3.1 → 3.3.2 - About page version-history leads with v3.3.2; v3.3.1 demoted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1692 lines
63 KiB
PHP
1692 lines
63 KiB
PHP
<?php
|
|
/**
|
|
* Logbook — WordPress work-logbook plugin
|
|
*
|
|
* Plugin Name: Logbook
|
|
* Plugin URI: https://icanhelp.ie/wp-notes
|
|
* Description: A lightweight task & logbook plugin for WordPress. Log your daily work, mark tasks done, and keep a tidy record inside the dashboard. Perfect for freelancers showing clients what's been delivered and students proving work to teachers.
|
|
* Version: 3.3.2
|
|
* Requires at least: 5.0
|
|
* Requires PHP: 7.2
|
|
* Author: IR240474
|
|
* Author URI: https://rangersmyth.xyz/
|
|
* License: GPL v2 or later
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
* Text Domain: a-wp-notes
|
|
* Domain Path: /languages
|
|
*
|
|
* @package WP_Notes
|
|
*/
|
|
|
|
/**
|
|
* Security check and WordPress core loading
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
die;
|
|
}
|
|
|
|
// One-time initialization of plugin constants
|
|
global $wp_notes_init;
|
|
if (!isset($wp_notes_init)) {
|
|
$wp_notes_init = true;
|
|
|
|
// Plugin Constants
|
|
if (!defined('WP_NOTES_VERSION')) define('WP_NOTES_VERSION', '3.3.2');
|
|
if (!defined('WP_NOTES_FILE')) define('WP_NOTES_FILE', __FILE__);
|
|
if (!defined('WP_NOTES_PATH')) define('WP_NOTES_PATH', plugin_dir_path(__FILE__));
|
|
if (!defined('WP_NOTES_URL')) define('WP_NOTES_URL', plugin_dir_url(__FILE__));
|
|
if (!defined('WP_NOTES_BASENAME')) define('WP_NOTES_BASENAME', plugin_basename(__FILE__));
|
|
}
|
|
|
|
// Initialize error logging if not already defined
|
|
if (!function_exists('wp_notes_log_error')) {
|
|
function wp_notes_log_error($message) {
|
|
if (defined('WP_DEBUG') && WP_DEBUG === true) {
|
|
error_log('[Logbook] ' . $message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Plugin class autoloader
|
|
spl_autoload_register(function ($class) {
|
|
$prefix = 'WP_Notes\\';
|
|
$base_dir = plugin_dir_path(__FILE__) . 'includes/';
|
|
|
|
$len = strlen($prefix);
|
|
if (strncmp($prefix, $class, $len) !== 0) {
|
|
return;
|
|
}
|
|
|
|
$relative_class = substr($class, $len);
|
|
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
|
|
|
if (file_exists($file)) {
|
|
require $file;
|
|
}
|
|
});
|
|
|
|
// Plugin initialization function
|
|
function wp_notes_init() {
|
|
// Load plugin functionality
|
|
if (class_exists('WP_Notes\\Plugin')) {
|
|
$plugin = new WP_Notes\Plugin();
|
|
$plugin->run();
|
|
}
|
|
}
|
|
|
|
// Hook into WordPress init
|
|
add_action('plugins_loaded', 'wp_notes_init', 10);
|
|
|
|
// Register activation and deactivation hooks
|
|
register_activation_hook(__FILE__, 'wp_notes_activate');
|
|
register_deactivation_hook(__FILE__, 'wp_notes_deactivate');
|
|
|
|
/**
|
|
* Plugin activation handler
|
|
*/
|
|
function wp_notes_activate() {
|
|
// Ensure PHP version is compatible
|
|
if (version_compare(PHP_VERSION, '7.2', '<')) {
|
|
deactivate_plugins(basename(__FILE__));
|
|
wp_die('This plugin requires PHP version 7.2 or higher.');
|
|
}
|
|
|
|
// Initialize plugin settings
|
|
add_option('wp_notes_version', WP_NOTES_VERSION);
|
|
|
|
// Create database tables
|
|
wp_notes_create_tables();
|
|
|
|
// Backup existing notes
|
|
$existing_notes = get_option('wp_notes', array());
|
|
$existing_done_notes = get_option('wp_done_notes', array());
|
|
|
|
if (!empty($existing_notes)) {
|
|
update_option('wp_notes_backup', $existing_notes);
|
|
}
|
|
if (!empty($existing_done_notes)) {
|
|
update_option('wp_done_notes_backup', $existing_done_notes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Plugin deactivation handler
|
|
*/
|
|
function wp_notes_deactivate() {
|
|
delete_option('wp_notes_version');
|
|
}
|
|
|
|
/**
|
|
* Create required database tables
|
|
*/
|
|
function wp_notes_create_tables() {
|
|
global $wpdb;
|
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$sql = array();
|
|
|
|
// Notes table
|
|
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wp_notes (
|
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
user_id bigint(20) NOT NULL,
|
|
note_content text NOT NULL,
|
|
note_status varchar(20) NOT NULL DEFAULT 'active',
|
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
KEY user_id (user_id),
|
|
KEY note_status (note_status)
|
|
) $charset_collate;";
|
|
|
|
foreach ($sql as $query) {
|
|
dbDelta($query);
|
|
}
|
|
}
|
|
|
|
// Initialize plugin
|
|
add_action('plugins_loaded', 'wp_notes_init');
|
|
|
|
// Set text domain for translations
|
|
function wp_notes_load_textdomain() {
|
|
load_plugin_textdomain('a-wp-notes', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
|
}
|
|
add_action('plugins_loaded', 'wp_notes_load_textdomain');
|
|
|
|
// Include required files
|
|
if (defined('WP_NOTES_PATH')) {
|
|
require_once WP_NOTES_PATH . 'inc/admin-bar.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-about.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-feedback.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-display.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-styles.php';
|
|
require_once WP_NOTES_PATH . 'inc/wp-notes-updater.php';
|
|
}
|
|
|
|
|
|
// Admin Menu
|
|
function wp_notes_admin_menu() {
|
|
add_menu_page(
|
|
'Logbook',
|
|
'Logbook',
|
|
'manage_options',
|
|
'wp-notes',
|
|
'wp_notes_page_callback',
|
|
'dashicons-admin-generic',
|
|
3
|
|
);
|
|
|
|
// "My Log" — the main landing submenu. Same slug as the parent
|
|
// menu so clicking either Logbook or My Log lands on the same
|
|
// central dashboard (the parent's wp_notes_page_callback already
|
|
// renders form + active list + completed list).
|
|
//
|
|
// CRITICAL: callback must be empty here. WordPress registers BOTH
|
|
// the parent's and the submenu's callbacks against the same page
|
|
// hook when slugs match, and runs them in order — passing
|
|
// wp_notes_create_page as the callback caused a duplicate
|
|
// "Create a New WP Note" form to render BELOW the main page
|
|
// content. Empty callback means only the parent's renderer fires.
|
|
add_submenu_page(
|
|
'wp-notes', // Parent slug
|
|
'My Log', // Page title (browser tab)
|
|
'My Log', // Menu label (sidebar)
|
|
'manage_options', // Capability
|
|
'wp-notes', // Menu slug (matches parent → same page)
|
|
'' // Empty callback — see comment above
|
|
);
|
|
|
|
// Settings submenu
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'Settings',
|
|
'Settings',
|
|
'manage_options',
|
|
'wp-notes-settings',
|
|
'wp_notes_settings_page'
|
|
);
|
|
|
|
// Import/Export submenu
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'Import/Export',
|
|
'Import/Export',
|
|
'manage_options',
|
|
'wp-notes-import-export',
|
|
'wp_notes_import_export_page'
|
|
);
|
|
|
|
// About submenu — the Logbook brand is already carried by the
|
|
// parent menu, so the submenu can be plain-spoken.
|
|
add_submenu_page(
|
|
'wp-notes',
|
|
'About', // Page title (browser tab)
|
|
'About', // Menu label (sidebar)
|
|
'manage_options',
|
|
'wp-notes-about',
|
|
'wp_notes_about_page' // Renderer in inc/wp-notes-about.php
|
|
);
|
|
|
|
// Register settings with validation callback
|
|
register_setting(
|
|
'wp_notes_settings',
|
|
'wp_notes_settings',
|
|
array(
|
|
'sanitize_callback' => 'wp_notes_validate_settings',
|
|
'default' => array(
|
|
'default_font' => 'Arial',
|
|
'default_size' => '16'
|
|
)
|
|
)
|
|
);
|
|
add_settings_section(
|
|
'wp_notes_main_section',
|
|
'General Settings',
|
|
'wp_notes_section_callback',
|
|
'wp-notes-settings'
|
|
);
|
|
add_settings_field(
|
|
'wp_notes_default_font',
|
|
'Default Font',
|
|
'wp_notes_font_callback',
|
|
'wp-notes-settings',
|
|
'wp_notes_main_section'
|
|
);
|
|
add_settings_field(
|
|
'wp_notes_default_size',
|
|
'Default Size',
|
|
'wp_notes_size_callback',
|
|
'wp-notes-settings',
|
|
'wp_notes_main_section'
|
|
);
|
|
}
|
|
|
|
// Settings validation callback
|
|
function wp_notes_validate_settings($input) {
|
|
$output = array();
|
|
|
|
// Validate font
|
|
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
|
|
$output['default_font'] = in_array($input['default_font'], $allowed_fonts) ? $input['default_font'] : 'Arial';
|
|
|
|
// Validate size (8-72px)
|
|
$size = absint($input['default_size']);
|
|
$output['default_size'] = ($size >= 8 && $size <= 72) ? $size : 16;
|
|
|
|
add_settings_error(
|
|
'wp_notes_settings',
|
|
'settings_updated',
|
|
'Settings saved successfully.',
|
|
'updated'
|
|
);
|
|
|
|
return $output;
|
|
}
|
|
|
|
// Settings page callback
|
|
function wp_notes_settings_page() {
|
|
// Show any settings messages
|
|
settings_errors('wp_notes_settings');
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Logbook Settings</h1>
|
|
<form method="post" action="options.php">
|
|
<?php
|
|
settings_fields('wp_notes_settings');
|
|
do_settings_sections('wp-notes-settings');
|
|
submit_button();
|
|
?>
|
|
</form>
|
|
|
|
<?php
|
|
// Updates panel — checks the Gitea repo for a newer release.
|
|
// Defined in inc/wp-notes-updater.php.
|
|
if ( function_exists( 'wp_notes_render_updates_panel' ) ) {
|
|
wp_notes_render_updates_panel();
|
|
}
|
|
?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// Settings section callback
|
|
function wp_notes_section_callback() {
|
|
echo '<p>Configure default settings for Logbook.</p>';
|
|
}
|
|
|
|
// Font setting callback
|
|
function wp_notes_font_callback() {
|
|
$options = get_option('wp_notes_settings');
|
|
$current = $options['default_font'] ?? 'Arial';
|
|
?>
|
|
<select name="wp_notes_settings[default_font]">
|
|
<option value="Arial" <?php selected($current, 'Arial'); ?>>Arial</option>
|
|
<option value="Helvetica" <?php selected($current, 'Helvetica'); ?>>Helvetica</option>
|
|
<option value="Times New Roman" <?php selected($current, 'Times New Roman'); ?>>Times New Roman</option>
|
|
<option value="Verdana" <?php selected($current, 'Verdana'); ?>>Verdana</option>
|
|
</select>
|
|
<?php
|
|
}
|
|
|
|
// Size setting callback
|
|
function wp_notes_size_callback() {
|
|
$options = get_option('wp_notes_settings');
|
|
$current = $options['default_size'] ?? '16';
|
|
?>
|
|
<input type="number" name="wp_notes_settings[default_size]" value="<?php echo esc_attr($current); ?>" min="8" max="72">
|
|
<span class="description">Font size in pixels (8-72)</span>
|
|
<?php
|
|
}
|
|
|
|
// Import/Export page callback
|
|
function wp_notes_import_export_page() {
|
|
// Display settings errors
|
|
settings_errors();
|
|
|
|
if (isset($_POST['export_notes'])) {
|
|
wp_notes_export_data();
|
|
}
|
|
|
|
if (isset($_POST['import_notes']) && !empty($_FILES['import_file']['tmp_name'])) {
|
|
try {
|
|
wp_notes_import_data();
|
|
} catch (Exception $e) {
|
|
add_settings_error(
|
|
'wp_notes_import',
|
|
'import_error',
|
|
'Error importing notes: ' . esc_html($e->getMessage()),
|
|
'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get current settings for display
|
|
$options = get_option('wp_notes_settings');
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Import/Export Notes</h1>
|
|
|
|
<!-- Export Section -->
|
|
<div class="postbox">
|
|
<div class="inside">
|
|
<h2>Export Notes</h2>
|
|
<p>Download your notes as JSON or CSV file.</p>
|
|
<form method="post">
|
|
<select name="export_format">
|
|
<option value="json">JSON</option>
|
|
<option value="csv">CSV</option>
|
|
</select>
|
|
<?php wp_nonce_field('wp_notes_export', 'export_nonce'); ?>
|
|
<input type="submit" name="export_notes" class="button button-primary" value="Export Notes">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Section -->
|
|
<div class="postbox">
|
|
<div class="inside">
|
|
<h2>Import Notes</h2>
|
|
<p>Import notes from JSON or CSV file.</p>
|
|
<form method="post" enctype="multipart/form-data">
|
|
<input type="file" name="import_file" accept=".json,.csv">
|
|
<?php wp_nonce_field('wp_notes_import', 'import_nonce'); ?>
|
|
<input type="submit" name="import_notes" class="button button-primary" value="Import Notes">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// Export functionality
|
|
function wp_notes_export_data() {
|
|
if (!isset($_POST['export_nonce']) || !wp_verify_nonce($_POST['export_nonce'], 'wp_notes_export')) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
$format = $_POST['export_format'] ?? 'json';
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
$data = array(
|
|
'active_notes' => $notes,
|
|
'completed_notes' => $done_notes,
|
|
'export_date' => current_time('mysql')
|
|
);
|
|
|
|
if ($format === 'json') {
|
|
header('Content-Type: application/json');
|
|
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.json');
|
|
echo wp_json_encode($data, JSON_PRETTY_PRINT);
|
|
} else {
|
|
header('Content-Type: text/csv');
|
|
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.csv');
|
|
$output = fopen('php://output', 'w');
|
|
fputcsv($output, array('Type', 'Text', 'Created By', 'Created On', 'Status'));
|
|
|
|
foreach ($notes as $note) {
|
|
fputcsv($output, array(
|
|
'active',
|
|
$note['text'],
|
|
$note['author_name'] ?? 'Unknown',
|
|
$note['timestamp'] ?? '',
|
|
'Active'
|
|
));
|
|
}
|
|
|
|
foreach ($done_notes as $note) {
|
|
fputcsv($output, array(
|
|
'completed',
|
|
$note['text'],
|
|
$note['author_name'] ?? 'Unknown',
|
|
$note['timestamp'] ?? '',
|
|
'Completed'
|
|
));
|
|
}
|
|
fclose($output);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// Import functionality
|
|
function wp_notes_import_data() {
|
|
if (!isset($_POST['import_nonce']) || !wp_verify_nonce($_POST['import_nonce'], 'wp_notes_import')) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
$file = $_FILES['import_file'];
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
|
|
if ($ext === 'json') {
|
|
$content = file_get_contents($file['tmp_name']);
|
|
$data = json_decode($content, true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
if (isset($data['active_notes'])) {
|
|
update_option('wp_notes', $data['active_notes']);
|
|
}
|
|
if (isset($data['completed_notes'])) {
|
|
update_option('wp_done_notes', $data['completed_notes']);
|
|
}
|
|
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
|
|
} else {
|
|
add_settings_error('wp_notes_import', 'import_error', 'Invalid JSON file');
|
|
}
|
|
} elseif ($ext === 'csv') {
|
|
$handle = fopen($file['tmp_name'], 'r');
|
|
$headers = fgetcsv($handle);
|
|
$notes = array();
|
|
$done_notes = array();
|
|
|
|
while (($data = fgetcsv($handle)) !== false) {
|
|
$note = array(
|
|
'text' => $data[1],
|
|
'author_name' => $data[2],
|
|
'timestamp' => $data[3],
|
|
'color' => '#000000',
|
|
'size' => '16',
|
|
'font' => 'Arial'
|
|
);
|
|
|
|
if ($data[0] === 'active') {
|
|
$notes[] = $note;
|
|
} else {
|
|
$done_notes[] = $note;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
|
|
} else {
|
|
add_settings_error('wp_notes_import', 'import_error', 'Invalid file format. Please use JSON or CSV files.');
|
|
}
|
|
}
|
|
add_action('admin_menu', 'wp_notes_admin_menu');
|
|
|
|
// The Tools → My Notes shortcut used to live here and routed to a
|
|
// separate bare-bones form at ?page=wp-notes-create. It was removed
|
|
// as a duplicate — the My Notes page (and the admin-bar "New Note"
|
|
// shortcut that jumps to its #new-note anchor) covers the same need
|
|
// with the proper UI. Any stale bookmark to the old URL is caught
|
|
// by wp_notes_redirect_legacy_create_page() below.
|
|
|
|
// Redirect anyone hitting the legacy ?page=wp-notes-create URL to
|
|
// the My Notes page, so existing bookmarks don't 404 / show "you do
|
|
// not have sufficient permissions" after the Tools shortcut removal.
|
|
add_action('admin_init', 'wp_notes_redirect_legacy_create_page');
|
|
function wp_notes_redirect_legacy_create_page() {
|
|
if (!is_admin()) { return; }
|
|
if (!isset($_GET['page'])) { return; }
|
|
if (sanitize_key(wp_unslash($_GET['page'])) !== 'wp-notes-create') { return; }
|
|
wp_safe_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
|
|
// Enqueue Scripts
|
|
function wp_notes_enqueue_scripts() {
|
|
// wp_enqueue_script('wp-notes-activity-tracker', WP_NOTES_URL . 'js/wp-notes-activity.js', ['jquery'], null, true); // Activity tracking removed 2025-05-09
|
|
wp_enqueue_script('wp-notes-feedback', WP_NOTES_URL . 'js/wp-notes-feedback.js', ['jquery'], null, true);
|
|
wp_localize_script('wp-notes-feedback', 'wp_notes_feedback_vars', array(
|
|
'ajax_url' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('wp_notes_feedback_nonce')
|
|
));
|
|
}
|
|
add_action('admin_enqueue_scripts', 'wp_notes_enqueue_scripts');
|
|
|
|
// Emoji picker initialization
|
|
add_action('admin_footer', 'wp_notes_emoji_picker_init');
|
|
function wp_notes_emoji_picker_init() {
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
// Initialize emoji picker
|
|
function initEmojiPicker() {
|
|
// Handle emoji picker toggle
|
|
$('.emoji-input').on('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
$('.emoji-picker-dropdown').not($container.find('.emoji-picker-dropdown')).hide();
|
|
$container.find('.emoji-picker-dropdown').toggle();
|
|
});
|
|
|
|
// Handle emoji selection
|
|
$('.emoji-option').on('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var emoji = $(this).data('emoji');
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
var $noteText = $('#wp_notes_text');
|
|
|
|
// Get cursor position or selection
|
|
var start = $noteText[0].selectionStart;
|
|
var end = $noteText[0].selectionEnd;
|
|
var text = $noteText.val();
|
|
|
|
// Insert emoji at cursor position
|
|
var newText = text.substring(0, start) + emoji + text.substring(end);
|
|
$noteText.val(newText);
|
|
|
|
// Update cursor position
|
|
var newCursorPos = start + emoji.length;
|
|
$noteText[0].setSelectionRange(newCursorPos, newCursorPos);
|
|
|
|
// Focus back on textarea
|
|
$noteText.focus();
|
|
|
|
// Hide dropdown
|
|
$container.find('.emoji-picker-dropdown').hide();
|
|
});
|
|
|
|
// Close emoji picker when clicking outside
|
|
$(document).on('click', function(e) {
|
|
if (!$(e.target).closest('.emoji-picker-container').length) {
|
|
$('.emoji-picker-dropdown').hide();
|
|
}
|
|
});
|
|
|
|
// Handle keyboard navigation — focus returns to the input
|
|
// (the picker trigger) after Escape now that the dedicated
|
|
// button has been removed.
|
|
$('.emoji-picker-container').on('keydown', function(e) {
|
|
var $dropdown = $(this).find('.emoji-picker-dropdown');
|
|
var $input = $(this).find('.emoji-input');
|
|
|
|
// Toggle dropdown with Enter or Space when the input is focused
|
|
if ((e.key === 'Enter' || e.key === ' ') && e.target === $input[0]) {
|
|
e.preventDefault();
|
|
$dropdown.toggle();
|
|
}
|
|
|
|
// Close with Escape
|
|
if (e.key === 'Escape' && $dropdown.is(':visible')) {
|
|
$dropdown.hide();
|
|
$input.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
initEmojiPicker();
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// JavaScript for saving edited notes
|
|
add_action('admin_footer', 'wp_notes_add_inline_scripts');
|
|
function wp_notes_add_inline_scripts() {
|
|
echo "
|
|
<script type='text/javascript'>
|
|
jQuery(document).ready(function($) {
|
|
// Show edit form
|
|
$('.edit-note').on('click', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
$('#edit-note-' + noteId).toggle();
|
|
});
|
|
|
|
// AJAX submit the edit form
|
|
$('.edit-note-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
var formData = $(this).serialize() + '&action=wp_notes_save_edit¬e_id=' + noteId;
|
|
$.post(ajaxurl, formData, function(response) {
|
|
if (response.success) {
|
|
alert('Note updated successfully!');
|
|
location.reload(); // Reload to display updated notes
|
|
} else {
|
|
alert('Error updating note: ' + response.data);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Persist the dismissal of the empty-state notice.
|
|
// WP core's common.js attaches the X button and hides the
|
|
// notice on click; we just hook the same click and fire an
|
|
// AJAX call to set the user_meta flag so the notice does
|
|
// not reappear on the next page load.
|
|
$(document).on('click', '.wp-notes-empty .notice-dismiss', function() {
|
|
var \$notice = $(this).closest('.wp-notes-empty');
|
|
var type = \$notice.data('wp-notes-empty-type');
|
|
var nonce = \$notice.data('wp-notes-nonce');
|
|
if (!type || !nonce) { return; }
|
|
$.post(ajaxurl, {
|
|
action: 'wp_notes_dismiss_empty',
|
|
type: type,
|
|
nonce: nonce
|
|
});
|
|
});
|
|
});
|
|
</script>";
|
|
}
|
|
|
|
// Receive the "Leave Feedback" form from the About page and forward
|
|
// it to the site admin via wp_mail(). Topics is an array of short
|
|
// keys (improve / help / bug / feature / use-case / thanks / other);
|
|
// message is an optional free-text textarea. Sender info is taken
|
|
// from wp_get_current_user() so we never trust client-supplied
|
|
// identity. Capability-gated to manage_options because the About
|
|
// page is admin-only.
|
|
add_action('wp_ajax_wp_notes_submit_feedback', 'wp_notes_ajax_submit_feedback');
|
|
function wp_notes_ajax_submit_feedback() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error('Insufficient permissions.', 403);
|
|
}
|
|
check_ajax_referer('wp_notes_feedback_submit', 'nonce');
|
|
|
|
$allowed_topics = ['improve', 'help', 'bug', 'feature', 'use-case', 'thanks', 'other'];
|
|
$raw_topics = isset($_POST['topics']) && is_array($_POST['topics']) ? wp_unslash($_POST['topics']) : [];
|
|
$topics = array_values(array_intersect($allowed_topics, array_map('sanitize_key', $raw_topics)));
|
|
|
|
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
|
|
|
|
if (empty($topics) && $message === '') {
|
|
wp_send_json_error('Pick at least one topic or write a message.', 400);
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
$site = get_bloginfo('name');
|
|
$to = get_option('admin_email');
|
|
|
|
$topic_labels = [
|
|
'improve' => 'Ideas to improve the plugin',
|
|
'help' => 'Needs help with the plugin',
|
|
'bug' => 'Reporting a bug',
|
|
'feature' => 'New feature request',
|
|
'use-case' => 'Sharing a use case',
|
|
'thanks' => 'Saying thanks',
|
|
'other' => 'Other',
|
|
];
|
|
$topics_pretty = array_map(function ($t) use ($topic_labels) { return $topic_labels[$t] ?? $t; }, $topics);
|
|
|
|
$subject = sprintf('[%s] Logbook feedback from %s', $site, $user->display_name ?: $user->user_login);
|
|
$body = "Feedback received via Logbook → About page\n";
|
|
$body .= str_repeat('-', 48) . "\n\n";
|
|
$body .= 'From: ' . ($user->display_name ?: $user->user_login) . ' <' . $user->user_email . ">\n";
|
|
$body .= 'Site: ' . home_url() . "\n";
|
|
$body .= 'Plugin: v' . WP_NOTES_VERSION . "\n\n";
|
|
$body .= "Topics:\n";
|
|
if (!empty($topics_pretty)) {
|
|
foreach ($topics_pretty as $label) { $body .= ' - ' . $label . "\n"; }
|
|
} else {
|
|
$body .= " (none selected)\n";
|
|
}
|
|
$body .= "\nMessage:\n";
|
|
$body .= $message !== '' ? $message . "\n" : "(no message provided)\n";
|
|
|
|
$headers = [
|
|
'Content-Type: text/plain; charset=UTF-8',
|
|
'Reply-To: ' . sanitize_email($user->user_email),
|
|
];
|
|
|
|
$sent = wp_mail($to, $subject, $body, $headers);
|
|
|
|
if (!$sent) {
|
|
wp_send_json_error('Email could not be sent. Check the site mail configuration.', 500);
|
|
}
|
|
|
|
wp_send_json_success(['delivered_to' => $to]);
|
|
}
|
|
|
|
// Persist a user's dismissal of the "No active/completed notes found"
|
|
// empty-state notice so it doesn't reappear on the next page load.
|
|
// Triggered by inline JS in wp_notes_add_inline_scripts() when the
|
|
// user clicks the X on a .wp-notes-empty notice.
|
|
add_action('wp_ajax_wp_notes_dismiss_empty', 'wp_notes_ajax_dismiss_empty');
|
|
function wp_notes_ajax_dismiss_empty() {
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_send_json_error('Insufficient permissions.', 403);
|
|
}
|
|
check_ajax_referer('wp_notes_dismiss_empty', 'nonce');
|
|
|
|
$type = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : '';
|
|
if (!in_array($type, ['active', 'completed'], true)) {
|
|
wp_send_json_error('Invalid list type.', 400);
|
|
}
|
|
|
|
update_user_meta(get_current_user_id(), 'wp_notes_dismissed_empty_' . $type, 1);
|
|
wp_send_json_success(['dismissed' => $type]);
|
|
}
|
|
|
|
// Handle saving edited notes
|
|
add_action('wp_ajax_wp_notes_save_edit', 'wp_notes_save_edit');
|
|
function wp_notes_save_edit() {
|
|
// Security checks
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_send_json_error('Insufficient permissions.');
|
|
return;
|
|
}
|
|
|
|
// Security check using nonce
|
|
check_ajax_referer('wp_notes_nonce', '_wpnonce');
|
|
|
|
// Validate required fields
|
|
$required_fields = ['note_id', 'new_text', 'edit_color', 'edit_size', 'edit_font'];
|
|
foreach ($required_fields as $field) {
|
|
if (!isset($_POST[$field])) {
|
|
wp_send_json_error("Missing required field: $field");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get and sanitize data
|
|
$note_id = absint($_POST['note_id']);
|
|
$new_text = sanitize_text_field($_POST['new_text']);
|
|
$new_color = sanitize_hex_color($_POST['edit_color']);
|
|
$new_size = absint($_POST['edit_size']);
|
|
|
|
// Validate font against allowed list
|
|
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
|
|
$new_font = in_array($_POST['edit_font'], $allowed_fonts) ? $_POST['edit_font'] : 'Arial';
|
|
|
|
// Validate size range
|
|
if ($new_size < 8 || $new_size > 72) {
|
|
wp_send_json_error('Font size must be between 8 and 72 pixels.');
|
|
return;
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (!isset($notes[$note_id])) {
|
|
wp_send_json_error('Note not found.');
|
|
return;
|
|
}
|
|
|
|
// Update note with validation
|
|
$notes[$note_id] = array_merge($notes[$note_id], [
|
|
'text' => $new_text,
|
|
'color' => $new_color ?: '#000000',
|
|
'size' => $new_size,
|
|
'font' => $new_font,
|
|
'modified_timestamp' => current_time('mysql'),
|
|
'modified_by' => get_current_user_id()
|
|
]);
|
|
|
|
if (!update_option('wp_notes', $notes)) {
|
|
wp_send_json_error('Failed to update note.');
|
|
return;
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => 'Note updated successfully',
|
|
'note' => $notes[$note_id]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Main Logbook page — central hub for note management
|
|
*/
|
|
function wp_notes_page_callback() {
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$total_notes = count($notes);
|
|
$total_done = count($done_notes);
|
|
|
|
// Get user settings
|
|
$settings = get_option('wp_notes_settings', array(
|
|
'default_font' => 'Arial',
|
|
'default_size' => '16'
|
|
));
|
|
?>
|
|
<div class="wrap">
|
|
<!-- Header Section with WordPress Admin Styling -->
|
|
<h1 class="wp-heading-inline">Logbook</h1>
|
|
<span class="page-title-action">v<?php echo esc_html(WP_NOTES_VERSION); ?></span>
|
|
<hr class="wp-header-end">
|
|
|
|
<p class="description" style="max-width: 720px;">
|
|
Log your daily tasks and keep a tidy work-log inside WordPress.
|
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-about')); ?>">Read more on the About page →</a>
|
|
</p>
|
|
|
|
<!-- Note Creation Form with WordPress Admin Styling -->
|
|
<div class="postbox">
|
|
<div class="postbox-header">
|
|
<h2 class="hndle">New Log Entry</h2>
|
|
</div>
|
|
<div class="inside">
|
|
<form method="post" class="wp-notes-form">
|
|
<div class="form-field">
|
|
<label for="wp_notes_text" class="screen-reader-text">Note Content:</label>
|
|
<textarea
|
|
name="wp_notes_text"
|
|
id="wp_notes_text"
|
|
class="large-text"
|
|
required
|
|
rows="4"
|
|
aria-label="Enter your note content"
|
|
placeholder="Enter your note here..."></textarea>
|
|
</div>
|
|
|
|
<div class="wp-notes-formatting form-field">
|
|
<fieldset>
|
|
<legend class="screen-reader-text">Note Formatting Options</legend>
|
|
|
|
<div class="formatting-options">
|
|
<div class="format-option">
|
|
<label for="wp_notes_color" class="format-label">
|
|
<span class="dashicons dashicons-color-picker"></span>
|
|
Text Color
|
|
</label>
|
|
<input
|
|
type="color"
|
|
name="wp_notes_color"
|
|
id="wp_notes_color"
|
|
value="#000000"
|
|
aria-label="Choose text color">
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_size" class="format-label">
|
|
<span class="dashicons dashicons-editor-textcolor"></span>
|
|
Size (px)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="wp_notes_size"
|
|
id="wp_notes_size"
|
|
min="8"
|
|
max="72"
|
|
value="16"
|
|
class="small-text"
|
|
aria-label="Choose text size">
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_font" class="format-label">
|
|
<span class="dashicons dashicons-editor-paragraph"></span>
|
|
Font
|
|
</label>
|
|
<select
|
|
name="wp_notes_font"
|
|
id="wp_notes_font"
|
|
class="regular-text"
|
|
aria-label="Choose font">
|
|
<option value="Arial">Arial</option>
|
|
<option value="Helvetica">Helvetica</option>
|
|
<option value="Times New Roman">Times New Roman</option>
|
|
<option value="Verdana">Verdana</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="format-option">
|
|
<label for="wp_notes_emoji" class="format-label">
|
|
<span class="dashicons dashicons-smiley"></span>
|
|
Emoji
|
|
</label>
|
|
<div class="emoji-picker-container">
|
|
<input
|
|
type="text"
|
|
name="wp_notes_emoji"
|
|
id="wp_notes_emoji"
|
|
placeholder="Click to add emoji"
|
|
class="small-text emoji-input"
|
|
aria-label="Add emoji"
|
|
readonly>
|
|
<div class="emoji-picker-dropdown" style="display: none;">
|
|
<div class="emoji-list">
|
|
<!-- Common emojis -->
|
|
<button type="button" class="emoji-option" data-emoji="😊">😊</button>
|
|
<button type="button" class="emoji-option" data-emoji="👍">👍</button>
|
|
<button type="button" class="emoji-option" data-emoji="✅">✅</button>
|
|
<button type="button" class="emoji-option" data-emoji="⭐">⭐</button>
|
|
<button type="button" class="emoji-option" data-emoji="📝">📝</button>
|
|
<button type="button" class="emoji-option" data-emoji="❗">❗</button>
|
|
<button type="button" class="emoji-option" data-emoji="❓">❓</button>
|
|
<button type="button" class="emoji-option" data-emoji="⚠️">⚠️</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
|
|
<div class="submit-button">
|
|
<input
|
|
type="submit"
|
|
value="Add Note"
|
|
class="button button-primary button-large"
|
|
aria-label="Add new note">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.wp-notes-form .form-field { margin-bottom: 1em; }
|
|
.wp-notes-formatting { background: #f9f9f9; padding: 15px; border-radius: 4px; }
|
|
.formatting-options { display: flex; gap: 20px; flex-wrap: wrap; align-items: center; }
|
|
.format-option { display: flex; flex-direction: column; gap: 5px; }
|
|
.format-label { display: flex; align-items: center; gap: 5px; }
|
|
.submit-button { margin-top: 1em; }
|
|
.emoji-picker-container { position: relative; }
|
|
.emoji-picker-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid #ccd0d4;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
z-index: 100;
|
|
}
|
|
.emoji-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 5px;
|
|
}
|
|
.emoji-option {
|
|
border: none;
|
|
background: none;
|
|
font-size: 20px;
|
|
padding: 5px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
.emoji-option:hover {
|
|
background: #f0f0f1;
|
|
}
|
|
.emoji-input {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<!-- Notes List -->
|
|
<h2>Log entries</h2>
|
|
<!-- Active Notes List -->
|
|
<div class="wp-notes-active" id="active-notes">
|
|
<?php wp_notes_display_notes('active'); ?>
|
|
</div>
|
|
|
|
<!-- Completed Notes List -->
|
|
<div class="wp-notes-completed" id="completed-notes">
|
|
<?php wp_notes_display_notes('completed'); ?>
|
|
</div>
|
|
|
|
<!-- Footer: support link (lives at the BOTTOM, not the top) -->
|
|
<p class="wp-notes-footer-support">
|
|
<a href="https://www.buymeacoffee.com/davidkeanek" target="_blank" rel="noopener"
|
|
class="button button-secondary"
|
|
aria-label="Support the developer by buying them a coffee">
|
|
☕ Buy me a coffee
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// List Table Function
|
|
function wp_notes_list_table() {
|
|
// Check user capabilities
|
|
if (!current_user_can('edit_posts')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
|
|
}
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (!$notes) {
|
|
echo '<p>' . esc_html__('No notes found.', 'a-wp-notes') . '</p>';
|
|
return;
|
|
}
|
|
|
|
/* Dashboard widget table. No checkbox column — bulk actions are not
|
|
wired up; re-add when there is something to act on. */
|
|
echo '<table class="wp-list-table widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" class="manage-column">Note</th>
|
|
<th scope="col" class="manage-column">Created By</th>
|
|
<th scope="col" class="manage-column">Created On</th>
|
|
<th scope="col" class="manage-column">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>';
|
|
|
|
foreach ($notes as $key => $note) {
|
|
$text = esc_html($note['text']);
|
|
$color = esc_attr($note['color'] ?? '#000000');
|
|
$size = esc_attr($note['size'] ?? '14');
|
|
$font = esc_attr($note['font'] ?? 'Arial');
|
|
$timestamp = esc_html($note['timestamp'] ?? current_time('mysql'));
|
|
$author = esc_html($note['author_name'] ?? 'Unknown');
|
|
|
|
echo "<tr>
|
|
<td style='color: $color; font-size: {$size}px; font-family: $font;'>$text</td>
|
|
<td>$author</td>
|
|
<td>$timestamp</td>
|
|
<td>
|
|
<form method='post' style='display:inline;'>
|
|
<input type='hidden' name='note_id' value='$key'>
|
|
<button type='submit' name='mark_done' value='Mark as Done' class='button button-secondary'>Mark as Done</button>
|
|
</form>
|
|
<button type='button' class='button edit-note' data-note-id='$key'>Edit</button>
|
|
<div id='edit-note-$key' style='display:none;'>
|
|
<form class='edit-note-form' data-note-id='$key'>
|
|
" . wp_nonce_field('wp_notes_nonce', '_wpnonce', true, false) . "
|
|
<input type='hidden' name='note_id' value='$key'>
|
|
<textarea name='new_text'>$text</textarea><br>
|
|
<input type='color' name='edit_color' value='$color'><br>
|
|
<input type='number' name='edit_size' value='$size' min='8' max='72'><br>
|
|
<select name='edit_font'>
|
|
<option value='Arial' " . selected('Arial', $font, false) . ">Arial</option>
|
|
<option value='Helvetica' " . selected('Helvetica', $font, false) . ">Helvetica</option>
|
|
<option value='Times New Roman' " . selected('Times New Roman', $font, false) . ">Times New Roman</option>
|
|
<option value='Verdana' " . selected('Verdana', $font, false) . ">Verdana</option>
|
|
</select><br>
|
|
<button type='submit' class='button button-primary'>Save</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>";
|
|
}
|
|
|
|
echo '</tbody></table>';
|
|
|
|
// JavaScript for form toggling and AJAX submissions
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
// Show edit form
|
|
$('.edit-note').on('click', function(e) {
|
|
e.preventDefault();
|
|
var noteId = $(this).data('note-id');
|
|
$('.edit-note-form').not('#edit-note-' + noteId).hide();
|
|
$('#edit-note-' + noteId).toggle();
|
|
});
|
|
|
|
// AJAX submit the edit form
|
|
$('.edit-note-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
var $form = $(this);
|
|
var $submitButton = $form.find('button[type="submit"]');
|
|
var noteId = $form.data('note-id');
|
|
|
|
// Log form data for debugging
|
|
console.log('Form data:', $form.serialize());
|
|
console.log('Note ID:', noteId);
|
|
|
|
// Disable submit button and show loading state
|
|
$submitButton.prop('disabled', true).text('Saving...');
|
|
|
|
// AJAX request
|
|
$.ajax({
|
|
url: ajaxurl,
|
|
type: 'POST',
|
|
data: $form.serialize() + '&action=wp_notes_save_edit¬e_id=' + noteId,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + (response.data || 'Unknown error occurred'));
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
alert('Server error: ' + error);
|
|
},
|
|
complete: function() {
|
|
$submitButton.prop('disabled', false).text('Save');
|
|
}
|
|
});
|
|
|
|
// Emoji Picker Functionality
|
|
$('.emoji-picker-button, .emoji-input').on('click', function(e) {
|
|
e.preventDefault();
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
$container.find('.emoji-picker-dropdown').toggle();
|
|
});
|
|
|
|
// Handle emoji selection
|
|
$('.emoji-option').on('click', function() {
|
|
var emoji = $(this).data('emoji');
|
|
var $container = $(this).closest('.emoji-picker-container');
|
|
var $input = $container.find('.emoji-input');
|
|
|
|
// Append emoji to note text
|
|
var $noteText = $('#wp_notes_text');
|
|
var currentPosition = $noteText[0].selectionStart;
|
|
var text = $noteText.val();
|
|
var newText = text.slice(0, currentPosition) + emoji + text.slice(currentPosition);
|
|
$noteText.val(newText);
|
|
|
|
// Update cursor position
|
|
$noteText[0].setSelectionRange(currentPosition + emoji.length, currentPosition + emoji.length);
|
|
$noteText.focus();
|
|
|
|
// Hide dropdown
|
|
$container.find('.emoji-picker-dropdown').hide();
|
|
});
|
|
|
|
// Close emoji picker when clicking outside
|
|
$(document).on('click', function(e) {
|
|
if (!$(e.target).closest('.emoji-picker-container').length) {
|
|
$('.emoji-picker-dropdown').hide();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// Done Notes Table Function
|
|
function wp_notes_done_table() {
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
if (!$done_notes) {
|
|
echo '<p>' . esc_html__('No completed notes.', 'a-wp-notes') . '</p>';
|
|
return;
|
|
}
|
|
|
|
echo '<form method="post"><table class="wp-list-table widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" class="manage-column column-cb check-column">
|
|
<input type="checkbox" id="select-all-done">
|
|
</th>
|
|
<th scope="col">Note</th>
|
|
<th scope="col">Created By</th>
|
|
<th scope="col">Created On</th>
|
|
<th scope="col">Completed By</th>
|
|
<th scope="col">Completed On</th>
|
|
<th scope="col">Last Modified</th>
|
|
<th scope="col">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>';
|
|
|
|
foreach ($done_notes as $key => $note) {
|
|
echo '<tr>
|
|
<td><input type="checkbox" name="done_ids[]" value="' . esc_attr($key) . '" class="done-checkbox"></td>
|
|
<td style="color: ' . esc_attr($note['color']) . '; font-size: 14px; font-family: ' . esc_attr($note['font']) . ';">'
|
|
. esc_html($note['text']) . '</td>
|
|
<td>' . esc_html($note['author_name'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['timestamp'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['completed_by'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['completed_on'] ?? 'Unknown') . '</td>
|
|
<td>' . esc_html($note['last_modified'] ?? 'N/A') . '</td>
|
|
<td>
|
|
<input type="submit" name="restore_note" value="Restore" class="button button-secondary">
|
|
</td>
|
|
</tr>';
|
|
}
|
|
|
|
echo '</tbody></table></form>';
|
|
|
|
// Add JavaScript for select all functionality
|
|
?>
|
|
<script type="text/javascript">
|
|
jQuery(document).ready(function($) {
|
|
$('#select-all-done').change(function() {
|
|
$('.done-checkbox').prop('checked', $(this).prop('checked'));
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// Handle note actions
|
|
add_action('admin_init', 'wp_notes_handle_actions');
|
|
function wp_notes_handle_actions() {
|
|
$current_user = wp_get_current_user();
|
|
|
|
// Check if we're adding a new note
|
|
if (isset($_POST['wp_notes_text'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
// Create the new note
|
|
$new_note = array(
|
|
'text' => sanitize_text_field($_POST['wp_notes_text']),
|
|
'color' => sanitize_hex_color($_POST['wp_notes_color']) ?: '#000000',
|
|
'size' => absint($_POST['wp_notes_size']) ?: 16,
|
|
'font' => sanitize_text_field($_POST['wp_notes_font']) ?: 'Arial',
|
|
'timestamp' => current_time('mysql'),
|
|
'author' => $current_user->ID,
|
|
'author_name' => $current_user->display_name
|
|
);
|
|
|
|
$notes[] = $new_note;
|
|
update_option('wp_notes', $notes);
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
// Handle "Mark as Done" submissions
|
|
if (isset($_POST['mark_done']) && isset($_POST['note_id'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$note_id = absint($_POST['note_id']);
|
|
|
|
if (isset($notes[$note_id])) {
|
|
$note = $notes[$note_id];
|
|
$note['completed_by'] = $current_user->display_name;
|
|
$note['completed_on'] = current_time('mysql');
|
|
$done_notes[] = $note;
|
|
unset($notes[$note_id]);
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Handle marking notes as done if 'mark_done' action is triggered
|
|
if (isset($_POST['mark_done']) && isset($_POST['note_ids'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
|
|
// Move selected notes to done list
|
|
foreach ($notes as $key => $note) {
|
|
if (in_array($key, $_POST['note_ids'])) {
|
|
$note['completed_by'] = $current_user->display_name;
|
|
$note['completed_on'] = current_time('mysql');
|
|
$done_notes[] = $note;
|
|
unset($notes[$key]); // Remove from active notes
|
|
}
|
|
}
|
|
|
|
// Update both lists in the database
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
|
|
// Redirect to avoid form resubmission
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
exit;
|
|
}
|
|
|
|
// Handle restore note action
|
|
if (isset($_POST['restore_note']) && isset($_POST['done_ids'])) {
|
|
$notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$current_user = wp_get_current_user();
|
|
$new_done_notes = array();
|
|
|
|
foreach ($done_notes as $key => $note) {
|
|
if (in_array($key, $_POST['done_ids'])) {
|
|
$note['last_modified'] = current_time('mysql');
|
|
$note['restored_by'] = $current_user->display_name;
|
|
$notes[] = $note;
|
|
} else {
|
|
$new_done_notes[] = $note;
|
|
}
|
|
}
|
|
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $new_done_notes);
|
|
}
|
|
|
|
// Handle edit note action
|
|
if (isset($_POST['edit_note']) && isset($_POST['note_ids'])) {
|
|
// Add edit functionality here
|
|
$notes = get_option('wp_notes', array());
|
|
$current_user = wp_get_current_user();
|
|
|
|
foreach ($_POST['note_ids'] as $key) {
|
|
if (isset($notes[$key])) {
|
|
$notes[$key]['last_modified'] = current_time('mysql');
|
|
$notes[$key]['modified_by'] = $current_user->display_name;
|
|
}
|
|
}
|
|
|
|
update_option('wp_notes', $notes);
|
|
}
|
|
}
|
|
|
|
// AJAX handler for editing a note
|
|
add_action('wp_ajax_wp_notes_edit_note', 'wp_notes_edit_note');
|
|
function wp_notes_edit_note() {
|
|
if (!isset($_POST['note_id']) || !isset($_POST['edit_text'])) {
|
|
wp_send_json_error('Invalid request');
|
|
return;
|
|
}
|
|
|
|
$note_id = absint($_POST['note_id']);
|
|
$new_text = sanitize_text_field($_POST['edit_text']);
|
|
$new_color = sanitize_hex_color($_POST['edit_color']);
|
|
$new_size = absint($_POST['edit_size']);
|
|
$new_font = sanitize_text_field($_POST['edit_font']);
|
|
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (isset($notes[$note_id])) {
|
|
$notes[$note_id]['text'] = $new_text;
|
|
$notes[$note_id]['color'] = $new_color;
|
|
$notes[$note_id]['size'] = $new_size;
|
|
$notes[$note_id]['font'] = $new_font;
|
|
|
|
update_option('wp_notes', $notes);
|
|
wp_send_json_success();
|
|
} else {
|
|
wp_send_json_error('Note not found.');
|
|
}
|
|
}
|
|
|
|
// Optimize the restore function for performance
|
|
function wp_notes_restore($note_id) {
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$notes = get_option('wp_notes', array());
|
|
|
|
if (isset($done_notes[$note_id])) {
|
|
$notes[$note_id] = $done_notes[$note_id];
|
|
unset($done_notes[$note_id]);
|
|
update_option('wp_notes', $notes);
|
|
update_option('wp_done_notes', $done_notes);
|
|
wp_redirect(admin_url('admin.php?page=wp-notes'));
|
|
}
|
|
}
|
|
|
|
// Dashboard widget
|
|
function wp_notes_dashboard_widget() {
|
|
wp_notes_list_table();
|
|
}
|
|
|
|
function wp_notes_add_dashboard_widgets() {
|
|
wp_add_dashboard_widget(
|
|
'wp_notes_dashboard_widget',
|
|
'Logbook',
|
|
'wp_notes_dashboard_widget'
|
|
);
|
|
}
|
|
add_action('wp_dashboard_setup', 'wp_notes_add_dashboard_widgets');
|
|
|
|
// Register Custom Post Type for notes
|
|
function wp_notes_register_cpt() {
|
|
$labels = array(
|
|
'name' => __('Notes', 'a-wp-notes'),
|
|
'singular_name' => __('Note', 'a-wp-notes'),
|
|
'menu_name' => __('Logbook', 'a-wp-notes'),
|
|
'add_new' => __('Add New', 'a-wp-notes'),
|
|
'add_new_item' => __('Add New Note', 'a-wp-notes'),
|
|
'edit_item' => __('Edit Note', 'a-wp-notes'),
|
|
'new_item' => __('New Note', 'a-wp-notes'),
|
|
'view_item' => __('View Note', 'a-wp-notes'),
|
|
'search_items' => __('Search Notes', 'a-wp-notes'),
|
|
'not_found' => __('No notes found', 'a-wp-notes'),
|
|
'not_found_in_trash'=> __('No notes found in trash', 'a-wp-notes'),
|
|
);
|
|
|
|
$args = array(
|
|
'labels' => $labels,
|
|
'public' => false,
|
|
// show_ui=false: the live UI stores notes in wp_options, not as
|
|
// CPT posts. show_ui=true caused WordPress to auto-inject "All
|
|
// Notes" and "Add New" submenus that pointed at post-new.php and
|
|
// routed users into the standard post editor — which writes to
|
|
// the wrong storage. The CPT remains registered so the
|
|
// wp_notes_migrate_to_cpt() helper can still use wp_insert_post.
|
|
'show_ui' => false,
|
|
'show_in_menu' => false,
|
|
'capability_type' => 'post',
|
|
'hierarchical' => false,
|
|
'supports' => array('title', 'editor', 'author', 'custom-fields'),
|
|
'has_archive' => false,
|
|
'menu_position' => null,
|
|
'show_in_rest' => false,
|
|
);
|
|
|
|
register_post_type('wp_note', $args);
|
|
|
|
// Register taxonomy for note categories
|
|
$tax_labels = array(
|
|
'name' => __('Note Categories', 'a-wp-notes'),
|
|
'singular_name' => __('Note Category', 'a-wp-notes'),
|
|
'search_items' => __('Search Categories', 'a-wp-notes'),
|
|
'all_items' => __('All Categories', 'a-wp-notes'),
|
|
'parent_item' => __('Parent Category', 'a-wp-notes'),
|
|
'parent_item_colon' => __('Parent Category:', 'a-wp-notes'),
|
|
'edit_item' => __('Edit Category', 'a-wp-notes'),
|
|
'update_item' => __('Update Category', 'a-wp-notes'),
|
|
'add_new_item' => __('Add New Category', 'a-wp-notes'),
|
|
'new_item_name' => __('New Category Name', 'a-wp-notes'),
|
|
'menu_name' => __('Categories', 'a-wp-notes'),
|
|
);
|
|
|
|
register_taxonomy('wp_note_category', 'wp_note', array(
|
|
'hierarchical' => true,
|
|
'labels' => $tax_labels,
|
|
// Hidden alongside the parent CPT — live UI doesn't surface
|
|
// categories yet, and showing the taxonomy in admin with the
|
|
// parent hidden would just dead-link.
|
|
'show_ui' => false,
|
|
'show_admin_column' => false,
|
|
'query_var' => true,
|
|
'show_in_rest' => false,
|
|
));
|
|
}
|
|
add_action('init', 'wp_notes_register_cpt');
|
|
|
|
// Add custom meta box for note properties
|
|
function wp_notes_add_meta_boxes() {
|
|
add_meta_box(
|
|
'wp_notes_properties',
|
|
__('Note Properties', 'a-wp-notes'),
|
|
'wp_notes_render_properties_meta_box',
|
|
'wp_note',
|
|
'side',
|
|
'high'
|
|
);
|
|
}
|
|
add_action('add_meta_boxes', 'wp_notes_add_meta_boxes');
|
|
|
|
// Render meta box content
|
|
function wp_notes_render_properties_meta_box($post) {
|
|
// Add nonce for security
|
|
wp_nonce_field('wp_notes_meta_box', 'wp_notes_meta_box_nonce');
|
|
|
|
// Get current values
|
|
$color = get_post_meta($post->ID, '_wp_note_color', true) ?: '#000000';
|
|
$size = get_post_meta($post->ID, '_wp_note_size', true) ?: '16';
|
|
$font = get_post_meta($post->ID, '_wp_note_font', true) ?: 'Arial';
|
|
$status = get_post_meta($post->ID, '_wp_note_status', true) ?: 'active';
|
|
?>
|
|
<p>
|
|
<label for="wp_note_color"><?php _e('Color:', 'a-wp-notes'); ?></label><br>
|
|
<input type="color" id="wp_note_color" name="wp_note_color" value="<?php echo esc_attr($color); ?>">
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_size"><?php _e('Size:', 'a-wp-notes'); ?></label><br>
|
|
<input type="number" id="wp_note_size" name="wp_note_size" value="<?php echo esc_attr($size); ?>" min="8" max="72">
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_font"><?php _e('Font:', 'a-wp-notes'); ?></label><br>
|
|
<select id="wp_note_font" name="wp_note_font">
|
|
<option value="Arial" <?php selected($font, 'Arial'); ?>>Arial</option>
|
|
<option value="Helvetica" <?php selected($font, 'Helvetica'); ?>>Helvetica</option>
|
|
<option value="Times New Roman" <?php selected($font, 'Times New Roman'); ?>>Times New Roman</option>
|
|
<option value="Verdana" <?php selected($font, 'Verdana'); ?>>Verdana</option>
|
|
</select>
|
|
</p>
|
|
<p>
|
|
<label for="wp_note_status"><?php _e('Status:', 'a-wp-notes'); ?></label><br>
|
|
<select id="wp_note_status" name="wp_note_status">
|
|
<option value="active" <?php selected($status, 'active'); ?>><?php _e('Active', 'a-wp-notes'); ?></option>
|
|
<option value="completed" <?php selected($status, 'completed'); ?>><?php _e('Completed', 'a-wp-notes'); ?></option>
|
|
</select>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
// Save meta box data
|
|
function wp_notes_save_meta_box_data($post_id) {
|
|
// Check if our nonce is set and verify it
|
|
if (!isset($_POST['wp_notes_meta_box_nonce']) ||
|
|
!wp_verify_nonce($_POST['wp_notes_meta_box_nonce'], 'wp_notes_meta_box')) {
|
|
return;
|
|
}
|
|
|
|
// Don't save during autosave
|
|
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
|
return;
|
|
}
|
|
|
|
// Check user permissions
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return;
|
|
}
|
|
|
|
// Save the meta fields
|
|
$fields = array(
|
|
'_wp_note_color' => 'sanitize_hex_color',
|
|
'_wp_note_size' => 'absint',
|
|
'_wp_note_font' => 'sanitize_text_field',
|
|
'_wp_note_status' => 'sanitize_text_field'
|
|
);
|
|
|
|
foreach ($fields as $key => $sanitize_callback) {
|
|
if (isset($_POST[str_replace('_', '', $key)])) {
|
|
$value = call_user_func($sanitize_callback, $_POST[str_replace('_', '', $key)]);
|
|
update_post_meta($post_id, $key, $value);
|
|
}
|
|
}
|
|
}
|
|
add_action('save_post_wp_note', 'wp_notes_save_meta_box_data');
|
|
|
|
// Data Migration Tool
|
|
function wp_notes_migrate_to_cpt() {
|
|
$existing_notes = get_option('wp_notes', array());
|
|
$done_notes = get_option('wp_done_notes', array());
|
|
$migrated_count = 0;
|
|
$errors = array();
|
|
|
|
// Start migration
|
|
foreach ($existing_notes as $note) {
|
|
$post_data = array(
|
|
'post_title' => wp_trim_words($note['text'], 10, '...'),
|
|
'post_content' => $note['text'],
|
|
'post_status' => 'publish',
|
|
'post_type' => 'wp_note',
|
|
'post_author' => $note['author'] ?? get_current_user_id(),
|
|
);
|
|
|
|
// Insert the post
|
|
$post_id = wp_insert_post($post_data, true);
|
|
|
|
if (!is_wp_error($post_id)) {
|
|
// Add note meta data
|
|
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
|
|
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
|
|
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
|
|
update_post_meta($post_id, '_wp_note_status', 'active');
|
|
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
|
|
$migrated_count++;
|
|
} else {
|
|
$errors[] = $post_id->get_error_message();
|
|
}
|
|
}
|
|
|
|
// Migrate completed notes
|
|
foreach ($done_notes as $note) {
|
|
$post_data = array(
|
|
'post_title' => wp_trim_words($note['text'], 10, '...'),
|
|
'post_content' => $note['text'],
|
|
'post_status' => 'publish',
|
|
'post_type' => 'wp_note',
|
|
'post_author' => $note['author'] ?? get_current_user_id(),
|
|
);
|
|
|
|
$post_id = wp_insert_post($post_data, true);
|
|
|
|
if (!is_wp_error($post_id)) {
|
|
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
|
|
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
|
|
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
|
|
update_post_meta($post_id, '_wp_note_status', 'completed');
|
|
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_completed_by', $note['completed_by'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_completed_on', $note['completed_on'] ?? '');
|
|
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
|
|
$migrated_count++;
|
|
} else {
|
|
$errors[] = $post_id->get_error_message();
|
|
}
|
|
}
|
|
|
|
// Create backup of old data
|
|
update_option('wp_notes_pre_migration_backup', array(
|
|
'notes' => $existing_notes,
|
|
'done_notes' => $done_notes,
|
|
'migration_date' => current_time('mysql')
|
|
));
|
|
|
|
return array(
|
|
'success' => true,
|
|
'migrated_count' => $migrated_count,
|
|
'errors' => $errors
|
|
);
|
|
}
|
|
|
|
// Add migration notice for admin
|
|
function wp_notes_migration_notice() {
|
|
if (!current_user_can('manage_options')) {
|
|
return;
|
|
}
|
|
|
|
$migrated = get_option('wp_notes_migration_completed');
|
|
if (!$migrated) {
|
|
?>
|
|
<div class="notice notice-info is-dismissible">
|
|
<p>
|
|
<?php _e('Logbook needs to migrate your existing notes to the new storage system.', 'a-wp-notes'); ?>
|
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-settings&action=migrate')); ?>" class="button button-primary">
|
|
<?php _e('Start Migration', 'a-wp-notes'); ?>
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
}
|
|
add_action('admin_notices', 'wp_notes_migration_notice');
|
|
|
|
// Handle migration request
|
|
function wp_notes_handle_migration() {
|
|
if (isset($_GET['page']) && $_GET['page'] === 'wp-notes-settings' &&
|
|
isset($_GET['action']) && $_GET['action'] === 'migrate') {
|
|
|
|
check_admin_referer('wp_notes_migration');
|
|
|
|
$result = wp_notes_migrate_to_cpt();
|
|
|
|
if ($result['success']) {
|
|
update_option('wp_notes_migration_completed', true);
|
|
add_settings_error(
|
|
'wp_notes_migration',
|
|
'migration_success',
|
|
sprintf(
|
|
__('Successfully migrated %d notes. %s', 'a-wp-notes'),
|
|
$result['migrated_count'],
|
|
!empty($result['errors']) ? 'Errors: ' . implode(', ', $result['errors']) : ''
|
|
),
|
|
'updated'
|
|
);
|
|
} else {
|
|
add_settings_error(
|
|
'wp_notes_migration',
|
|
'migration_error',
|
|
__('Migration failed. Please try again or contact support.', 'a-wp-notes'),
|
|
'error'
|
|
);
|
|
}
|
|
|
|
wp_redirect(admin_url('admin.php?page=wp-notes-settings'));
|
|
exit;
|
|
}
|
|
}
|
|
add_action('admin_init', 'wp_notes_handle_migration');
|
|
|
|
// End of wp-notes.php
|
|
?>
|