|
@@ -14,6 +14,8 @@
|
|
|
#include <iomanip>
|
|
#include <iomanip>
|
|
|
#include <sstream>
|
|
#include <sstream>
|
|
|
#include <set>
|
|
#include <set>
|
|
|
|
|
+#include <utime.h>
|
|
|
|
|
+#include <time.h>
|
|
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
@@ -77,7 +79,7 @@ struct LoadedItem {
|
|
|
class RenamerWindow : public Gtk::Window {
|
|
class RenamerWindow : public Gtk::Window {
|
|
|
public:
|
|
public:
|
|
|
RenamerWindow() : m_Dispatcher() {
|
|
RenamerWindow() : m_Dispatcher() {
|
|
|
- set_title("Simple Image Renamer");
|
|
|
|
|
|
|
+ set_title("Simple Image Renamer 1.1");
|
|
|
set_default_size(1280, 850);
|
|
set_default_size(1280, 850);
|
|
|
set_wmclass("simpleimagerenamer", "simpleimagerenamer");
|
|
set_wmclass("simpleimagerenamer", "simpleimagerenamer");
|
|
|
|
|
|
|
@@ -112,7 +114,7 @@ public:
|
|
|
m_ScaleZoom.signal_value_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
|
|
m_ScaleZoom.signal_value_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
|
|
|
m_ToolbarTop.pack_start(m_ScaleZoom, Gtk::PACK_SHRINK, 5);
|
|
m_ToolbarTop.pack_start(m_ScaleZoom, Gtk::PACK_SHRINK, 5);
|
|
|
|
|
|
|
|
- m_BtnUndo.set_label("Undo");
|
|
|
|
|
|
|
+ m_BtnUndo.set_label("Undo Rename");
|
|
|
m_BtnUndo.set_sensitive(false);
|
|
m_BtnUndo.set_sensitive(false);
|
|
|
m_BtnUndo.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_undo_clicked));
|
|
m_BtnUndo.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_undo_clicked));
|
|
|
m_ToolbarTop.pack_end(m_BtnUndo, Gtk::PACK_SHRINK, 5);
|
|
m_ToolbarTop.pack_end(m_BtnUndo, Gtk::PACK_SHRINK, 5);
|
|
@@ -123,7 +125,9 @@ public:
|
|
|
m_ToolbarControls.set_margin_top(10); m_ToolbarControls.set_margin_bottom(10);
|
|
m_ToolbarControls.set_margin_top(10); m_ToolbarControls.set_margin_bottom(10);
|
|
|
m_FrameControls.add(m_ToolbarControls);
|
|
m_FrameControls.add(m_ToolbarControls);
|
|
|
|
|
|
|
|
- m_ComboMode.append("Sequential"); m_ComboMode.append("Replace");
|
|
|
|
|
|
|
+ m_ComboMode.append("Sequential");
|
|
|
|
|
+ m_ComboMode.append("Replace");
|
|
|
|
|
+ m_ComboMode.append("Change Date");
|
|
|
m_ComboMode.set_active(0);
|
|
m_ComboMode.set_active(0);
|
|
|
m_ComboMode.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_mode_changed));
|
|
m_ComboMode.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_mode_changed));
|
|
|
m_ToolbarControls.pack_start(m_ComboMode, Gtk::PACK_SHRINK, 5);
|
|
m_ToolbarControls.pack_start(m_ComboMode, Gtk::PACK_SHRINK, 5);
|
|
@@ -149,6 +153,13 @@ public:
|
|
|
m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label("Replace:")), Gtk::PACK_SHRINK, 5);
|
|
m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label("Replace:")), Gtk::PACK_SHRINK, 5);
|
|
|
m_BoxReplace.pack_start(m_EntryReplace, Gtk::PACK_EXPAND_WIDGET, 5);
|
|
m_BoxReplace.pack_start(m_EntryReplace, Gtk::PACK_EXPAND_WIDGET, 5);
|
|
|
m_StackModes.add(m_BoxReplace, "Replace");
|
|
m_StackModes.add(m_BoxReplace, "Replace");
|
|
|
|
|
+
|
|
|
|
|
+ // --- Change Date Mode ---
|
|
|
|
|
+ m_EntryDate.set_placeholder_text("YYYY-MM-DD HH:MM:SS (Empty = 1970)");
|
|
|
|
|
+ m_EntryDate.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
|
|
|
|
|
+ m_BoxDate.pack_start(*Gtk::manage(new Gtk::Label("Target Date:")), Gtk::PACK_SHRINK, 5);
|
|
|
|
|
+ m_BoxDate.pack_start(m_EntryDate, Gtk::PACK_EXPAND_WIDGET, 5);
|
|
|
|
|
+ m_StackModes.add(m_BoxDate, "Date");
|
|
|
|
|
|
|
|
m_ToolbarControls.pack_start(m_StackModes, Gtk::PACK_EXPAND_WIDGET, 5);
|
|
m_ToolbarControls.pack_start(m_StackModes, Gtk::PACK_EXPAND_WIDGET, 5);
|
|
|
|
|
|
|
@@ -210,10 +221,10 @@ public:
|
|
|
~RenamerWindow() { m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join(); }
|
|
~RenamerWindow() { m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join(); }
|
|
|
|
|
|
|
|
protected:
|
|
protected:
|
|
|
- Gtk::Box m_VBox, m_ToolbarTop, m_ToolbarControls, m_BoxPattern, m_BoxReplace;
|
|
|
|
|
|
|
+ Gtk::Box m_VBox, m_ToolbarTop, m_ToolbarControls, m_BoxPattern, m_BoxReplace, m_BoxDate;
|
|
|
Gtk::Frame m_FrameControls;
|
|
Gtk::Frame m_FrameControls;
|
|
|
Gtk::Button m_BtnOpen, m_BtnRename, m_BtnUndo;
|
|
Gtk::Button m_BtnOpen, m_BtnRename, m_BtnUndo;
|
|
|
- Gtk::Entry m_EntryPattern, m_EntryFind, m_EntryReplace;
|
|
|
|
|
|
|
+ Gtk::Entry m_EntryPattern, m_EntryFind, m_EntryReplace, m_EntryDate;
|
|
|
Gtk::ComboBoxText m_ComboSort, m_ComboMode;
|
|
Gtk::ComboBoxText m_ComboSort, m_ComboMode;
|
|
|
Gtk::Scale m_ScaleZoom;
|
|
Gtk::Scale m_ScaleZoom;
|
|
|
Gtk::SpinButton m_SpinStartNum;
|
|
Gtk::SpinButton m_SpinStartNum;
|
|
@@ -300,6 +311,17 @@ protected:
|
|
|
std::set<std::string> seen;
|
|
std::set<std::string> seen;
|
|
|
for (auto row : m_RefListStore->children()) {
|
|
for (auto row : m_RefListStore->children()) {
|
|
|
std::string orig = row[m_Columns.m_col_filename], next = orig;
|
|
std::string orig = row[m_Columns.m_col_filename], next = orig;
|
|
|
|
|
+
|
|
|
|
|
+ if (mode == 2 && row[m_Columns.m_col_checked]) {
|
|
|
|
|
+ std::string input = m_EntryDate.get_text();
|
|
|
|
|
+ if (input.empty()) input = "1970-01-01 00:00:00";
|
|
|
|
|
+ std::stringstream mu;
|
|
|
|
|
+ mu << "<span font='9'><span weight='bold'>" << Glib::Markup::escape_text(orig) << "</span>\n";
|
|
|
|
|
+ mu << "<span foreground='#e01b24' weight='bold'>DATE INJECTION: " << Glib::Markup::escape_text(input) << "</span></span>";
|
|
|
|
|
+ row[m_Columns.m_col_markup] = mu.str();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (row[m_Columns.m_col_checked]) {
|
|
if (row[m_Columns.m_col_checked]) {
|
|
|
if (mode == 0 && !pat.empty()) {
|
|
if (mode == 0 && !pat.empty()) {
|
|
|
std::string num = std::to_string(start + count++);
|
|
std::string num = std::to_string(start + count++);
|
|
@@ -321,7 +343,58 @@ protected:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void on_rename_execute() {
|
|
void on_rename_execute() {
|
|
|
- UndoStep step; int mode = m_ComboMode.get_active_row_number(), start = m_SpinStartNum.get_value_as_int(), count = 0;
|
|
|
|
|
|
|
+ int mode = m_ComboMode.get_active_row_number();
|
|
|
|
|
+
|
|
|
|
|
+ // Handle Date Application logic
|
|
|
|
|
+ if (mode == 2) {
|
|
|
|
|
+ std::string input = m_EntryDate.get_text();
|
|
|
|
|
+ if (input.empty()) input = "1970-01-01 00:00:00";
|
|
|
|
|
+
|
|
|
|
|
+ struct tm tm = {};
|
|
|
|
|
+ if (!strptime(input.c_str(), "%Y-%m-%d %H:%M:%S", &tm)) {
|
|
|
|
|
+ m_Statusbar.push("Error: Invalid date format. Use YYYY-MM-DD HH:MM:SS");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ time_t target_epoch = mktime(&tm);
|
|
|
|
|
+ char exif_buf[32];
|
|
|
|
|
+ strftime(exif_buf, sizeof(exif_buf), "%Y:%m:%d %H:%M:%S", &tm);
|
|
|
|
|
+ std::string exif_date = exif_buf;
|
|
|
|
|
+
|
|
|
|
|
+ for (auto row : m_RefListStore->children()) {
|
|
|
|
|
+ if (row[m_Columns.m_col_checked]) {
|
|
|
|
|
+ std::string file_path = (std::string)row[m_Columns.m_col_path];
|
|
|
|
|
+
|
|
|
|
|
+ // Rewrite EXIF via exiv2
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto img = Exiv2::ImageFactory::open(file_path);
|
|
|
|
|
+ if (img->good()) {
|
|
|
|
|
+ img->readMetadata();
|
|
|
|
|
+ Exiv2::ExifData &exifData = img->exifData();
|
|
|
|
|
+ exifData["Exif.Image.DateTime"] = exif_date;
|
|
|
|
|
+ exifData["Exif.Photo.DateTimeOriginal"] = exif_date;
|
|
|
|
|
+ exifData["Exif.Photo.DateTimeDigitized"] = exif_date;
|
|
|
|
|
+ img->setExifData(exifData);
|
|
|
|
|
+ img->writeMetadata();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ // Fallthrough, skip EXIF if format doesn't support it or is violently corrupted
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Rewrite Filesystem mtime and atime
|
|
|
|
|
+ struct utimbuf new_times;
|
|
|
|
|
+ new_times.actime = target_epoch;
|
|
|
|
|
+ new_times.modtime = target_epoch;
|
|
|
|
|
+ utime(file_path.c_str(), &new_times);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ m_Statusbar.push("Dates Updated (EXIF + FS).");
|
|
|
|
|
+ start_loading_folder(m_current_path); // Refresh UI to pull new metadata
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Standard Rename Logic
|
|
|
|
|
+ UndoStep step; int start = m_SpinStartNum.get_value_as_int(), count = 0;
|
|
|
std::string pat = m_EntryPattern.get_text();
|
|
std::string pat = m_EntryPattern.get_text();
|
|
|
size_t hpos = pat.find('#');
|
|
size_t hpos = pat.find('#');
|
|
|
int pad = (hpos != std::string::npos) ? (pat.find_last_of('#') - hpos + 1) : 0;
|
|
int pad = (hpos != std::string::npos) ? (pat.find_last_of('#') - hpos + 1) : 0;
|
|
@@ -415,7 +488,7 @@ protected:
|
|
|
m_current_path = p; m_RefListStore->clear(); std::vector<fs::path> f;
|
|
m_current_path = p; m_RefListStore->clear(); std::vector<fs::path> f;
|
|
|
for (const auto& e : fs::directory_iterator(p)) {
|
|
for (const auto& e : fs::directory_iterator(p)) {
|
|
|
std::string ext = e.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
std::string ext = e.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
|
- if (ext == ".jpg" || ext == ".png" || ext == ".jpeg") f.push_back(e.path());
|
|
|
|
|
|
|
+ if (ext == ".jpg" || ext == ".png" || ext == ".jpeg" || ext == ".webp" || ext == ".tif" || ext == ".tiff") f.push_back(e.path());
|
|
|
}
|
|
}
|
|
|
m_total_files = f.size(); m_processed_files = 0;
|
|
m_total_files = f.size(); m_processed_files = 0;
|
|
|
if (m_WorkerThread.joinable()) { m_stop_flag = true; m_WorkerThread.join(); }
|
|
if (m_WorkerThread.joinable()) { m_stop_flag = true; m_WorkerThread.join(); }
|
|
@@ -435,13 +508,23 @@ protected:
|
|
|
|
|
|
|
|
void on_open_folder_clicked() {
|
|
void on_open_folder_clicked() {
|
|
|
Gtk::FileChooserDialog d(*this, "Open", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
|
|
Gtk::FileChooserDialog d(*this, "Open", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
|
|
|
- d.set_show_hidden(true); // <--- Here's your fix
|
|
|
|
|
|
|
+ d.set_show_hidden(true);
|
|
|
d.add_button("Cancel", Gtk::RESPONSE_CANCEL); d.add_button("Open", Gtk::RESPONSE_OK);
|
|
d.add_button("Cancel", Gtk::RESPONSE_CANCEL); d.add_button("Open", Gtk::RESPONSE_OK);
|
|
|
if (d.run() == Gtk::RESPONSE_OK) start_loading_folder(d.get_filename());
|
|
if (d.run() == Gtk::RESPONSE_OK) start_loading_folder(d.get_filename());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void on_selection_change(bool s) { for (auto row : m_RefListStore->children()) row[m_Columns.m_col_checked] = s; update_preview(); }
|
|
void on_selection_change(bool s) { for (auto row : m_RefListStore->children()) row[m_Columns.m_col_checked] = s; update_preview(); }
|
|
|
- void on_mode_changed() { m_StackModes.set_visible_child(m_ComboMode.get_active_row_number() == 0 ? "Pattern" : "Replace"); update_preview(); }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ void on_mode_changed() {
|
|
|
|
|
+ int mode = m_ComboMode.get_active_row_number();
|
|
|
|
|
+ if (mode == 0) m_StackModes.set_visible_child("Pattern");
|
|
|
|
|
+ else if (mode == 1) m_StackModes.set_visible_child("Replace");
|
|
|
|
|
+ else if (mode == 2) m_StackModes.set_visible_child("Date");
|
|
|
|
|
+
|
|
|
|
|
+ m_BtnRename.set_label(mode == 2 ? "Apply Target Date (EXIF+FS)" : "Apply New Names");
|
|
|
|
|
+ update_preview();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void on_cell_toggled(const Glib::ustring& p) { auto it = m_RefListStore->get_iter(Gtk::TreePath(p)); if(it) { (*it)[m_Columns.m_col_checked] = !(*it)[m_Columns.m_col_checked]; update_preview(); } }
|
|
void on_cell_toggled(const Glib::ustring& p) { auto it = m_RefListStore->get_iter(Gtk::TreePath(p)); if(it) { (*it)[m_Columns.m_col_checked] = !(*it)[m_Columns.m_col_checked]; update_preview(); } }
|
|
|
bool on_iconview_button_press(GdkEventButton* e) { if (e->button == 3) { m_MenuPopup.popup(e->button, e->time); return true; } return false; }
|
|
bool on_iconview_button_press(GdkEventButton* e) { if (e->button == 3) { m_MenuPopup.popup(e->button, e->time); return true; } return false; }
|
|
|
};
|
|
};
|